@adecore/editor-react 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PickPopup.js +94 -73
- package/dist/completion.d.ts +28 -0
- package/dist/completion.js +76 -15
- package/dist/editor-language.d.ts +4 -0
- package/dist/editor-language.js +6 -0
- package/dist/host-types.d.ts +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/key-priority.d.ts +1 -0
- package/dist/key-priority.js +2 -0
- package/dist/live-templates.d.ts +16 -0
- package/dist/live-templates.js +289 -0
- package/dist/locales/en.json +30 -0
- package/dist/locales/nl.json +30 -0
- package/dist/markup-context.d.ts +18 -0
- package/dist/markup-context.js +122 -5
- package/dist/markup.d.ts +3 -0
- package/dist/markup.js +16 -4
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +1 -0
- package/dist/postfix.d.ts +21 -0
- package/dist/postfix.js +212 -0
- package/dist/project-language.d.ts +2 -0
- package/dist/project-language.js +3 -0
- package/dist/shortcut-keys.d.ts +5 -0
- package/dist/snippet-insert.d.ts +10 -0
- package/dist/snippet-insert.js +217 -0
- package/dist/snippet.d.ts +7 -1
- package/dist/snippet.js +65 -8
- package/dist/surround-constructs.d.ts +10 -0
- package/dist/surround-constructs.js +100 -0
- package/dist/surround.d.ts +18 -0
- package/dist/surround.js +399 -0
- package/dist/template-builtins.d.ts +3 -0
- package/dist/template-builtins.js +46 -0
- package/dist/template-context.d.ts +12 -0
- package/dist/template-context.js +56 -0
- package/dist/templates.d.ts +59 -0
- package/dist/templates.js +103 -0
- package/package.json +6 -6
- package/src/PickPopup.tsx +7 -0
- package/src/completion.ts +111 -13
- package/src/editor-language.ts +6 -0
- package/src/host-types.ts +7 -0
- package/src/index.ts +16 -0
- package/src/key-priority.ts +2 -0
- package/src/live-templates.ts +315 -0
- package/src/locales/en.json +30 -0
- package/src/locales/nl.json +30 -0
- package/src/markup-context.ts +131 -6
- package/src/markup.ts +15 -5
- package/src/pick.ts +17 -1
- package/src/popups.ts +2 -0
- package/src/postfix.ts +224 -0
- package/src/project-language.ts +3 -0
- package/src/snippet-insert.ts +258 -0
- package/src/snippet.ts +82 -7
- package/src/surround-constructs.ts +85 -0
- package/src/surround.ts +430 -0
- package/src/template-builtins.ts +94 -0
- package/src/template-context.ts +65 -0
- package/src/templates.ts +197 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { identifierPrefix, isIdentifierCharacter } from './completion-model.js';
|
|
2
|
+
import { comparePositions } from './diagnostics-model.js';
|
|
3
|
+
import { KEY_PRIORITY } from './key-priority.js';
|
|
4
|
+
import { textBeforeOf } from './line-text.js';
|
|
5
|
+
import { isShortcut } from './shortcut-keys.js';
|
|
6
|
+
import { PostfixFeature } from './postfix.js';
|
|
7
|
+
import { bodyWithIndentUnit, dedentLines, indentUnitOf, insertSnippet } from './snippet-insert.js';
|
|
8
|
+
import { escapeSnippetText } from './snippet.js';
|
|
9
|
+
import { SNIPPET_KIND, sortAfterServer, templateApplies, templateContextAt } from './template-context.js';
|
|
10
|
+
const TOAST_ID = 'language-templates';
|
|
11
|
+
const IDENTIFIER_START = /[\p{L}_$]/u;
|
|
12
|
+
/* Sorts the rows of a list of templates by group, then key. */
|
|
13
|
+
function grouped(template) {
|
|
14
|
+
return `${template.group ?? template.languages[0]}\u0000${template.key}`;
|
|
15
|
+
}
|
|
16
|
+
/* A key that starts in the middle of a word is no key, unless it starts with a character no word has. */
|
|
17
|
+
function startsInsideWord(before, index) {
|
|
18
|
+
return index > 0 && IDENTIFIER_START.test(before[index]) && isIdentifierCharacter(before[index - 1]);
|
|
19
|
+
}
|
|
20
|
+
/* The longest start of `key` that the text before the caret ends with, as a number of characters; 0 when none. */
|
|
21
|
+
function prefixLength(key, before) {
|
|
22
|
+
for (let length = Math.min(key.length, before.length); length >= 1; length--) {
|
|
23
|
+
if (before.endsWith(key.slice(0, length)) && !startsInsideWord(before, before.length - length)) {
|
|
24
|
+
return length;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
/*
|
|
30
|
+
* Live templates: a key typed before the caret and Tab expands a snippet, Insert Live Template lists the ones that
|
|
31
|
+
* apply, and Save as Live Template hands a selection to the host. The templates come from the registry of the
|
|
32
|
+
* project. Tab comes after a running snippet and before Emmet, and a key that starts before an Emmet abbreviation
|
|
33
|
+
* wins over it, as in IntelliJ.
|
|
34
|
+
*/
|
|
35
|
+
export class TemplatesFeature {
|
|
36
|
+
language;
|
|
37
|
+
postfix;
|
|
38
|
+
constructor(language) {
|
|
39
|
+
this.language = language;
|
|
40
|
+
this.postfix = new PostfixFeature(language);
|
|
41
|
+
language.onDispose(language.onKey(event => {
|
|
42
|
+
if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.isComposing) {
|
|
43
|
+
return this.expand();
|
|
44
|
+
}
|
|
45
|
+
if (isShortcut(language.shortcuts.insertLiveTemplate, event)) {
|
|
46
|
+
this.insert();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (isShortcut(language.shortcuts.saveAsLiveTemplate, event)) {
|
|
50
|
+
this.saveSelection();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}, KEY_PRIORITY.templates));
|
|
55
|
+
language.onDispose(language.completion.addSource({
|
|
56
|
+
items: request => [...this.rows(request.position), ...this.postfix.rows(request.position)]
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/*
|
|
60
|
+
* Expands the template whose key ends at the caret, or lists them when several share the key. False where Tab has
|
|
61
|
+
* nothing to expand, and where the key is an Emmet abbreviation, which the markup feature expands on the same Tab.
|
|
62
|
+
*/
|
|
63
|
+
expand() {
|
|
64
|
+
const {
|
|
65
|
+
editor,
|
|
66
|
+
completion
|
|
67
|
+
} = this.language;
|
|
68
|
+
const selections = editor.getSelections();
|
|
69
|
+
if (editor.isReadOnly?.() === true || selections.some(selection => comparePositions(selection.start, selection.end) !== 0)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const match = this.matchAtCarets(selections.map(selection => selection.start));
|
|
73
|
+
const custom = selections.length === 1 ? this.customKey() : null;
|
|
74
|
+
if (custom !== null && (match === null || comparePositions(match.start, custom.start) >= 0)) {
|
|
75
|
+
return custom.expand?.() ?? false;
|
|
76
|
+
}
|
|
77
|
+
if (match === null) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (match.templates.length === 1) {
|
|
81
|
+
this.expandAtCarets(match.templates[0], match.start);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
completion.showItems(match.templates.map(template => this.row(template, template.key, grouped(template), {
|
|
85
|
+
start: match.start,
|
|
86
|
+
end: editor.getCaret()
|
|
87
|
+
}, () => this.expandAtCarets(template, match.start))), match.start);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/* Lists the templates that apply, only the ones whose key or description fits the word before the caret when some do. */
|
|
91
|
+
insert() {
|
|
92
|
+
const {
|
|
93
|
+
editor,
|
|
94
|
+
completion,
|
|
95
|
+
project
|
|
96
|
+
} = this.language;
|
|
97
|
+
if (editor.isReadOnly?.() === true) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const selection = editor.getSelection();
|
|
101
|
+
const selected = comparePositions(selection.start, selection.end) !== 0;
|
|
102
|
+
const caret = editor.getCaret();
|
|
103
|
+
const before = textBeforeOf(editor, caret);
|
|
104
|
+
const word = selected ? '' : identifierPrefix(before);
|
|
105
|
+
const wordStart = {
|
|
106
|
+
line: caret.line,
|
|
107
|
+
character: caret.character - word.length
|
|
108
|
+
};
|
|
109
|
+
const context = templateContextAt(this.language, selected ? selection.start : wordStart);
|
|
110
|
+
const applicable = project.templates.live.filter(template => templateApplies(template, context));
|
|
111
|
+
if (applicable.length === 0) {
|
|
112
|
+
const title = project.i18n.t('editor:language.templates.none');
|
|
113
|
+
project.host.notify?.({
|
|
114
|
+
id: TOAST_ID,
|
|
115
|
+
kind: 'error',
|
|
116
|
+
title
|
|
117
|
+
});
|
|
118
|
+
editor.announce(title);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const keyed = template => word !== '' && prefixLength(template.key, before) > 0;
|
|
122
|
+
const described = template => word !== '' && (template.description?.toLowerCase().includes(word.toLowerCase()) ?? false);
|
|
123
|
+
const matching = applicable.filter(template => keyed(template) || described(template));
|
|
124
|
+
const start = matching.length === 0 ? caret : wordStart;
|
|
125
|
+
const text = selected ? editor.textInRange(selection) : '';
|
|
126
|
+
const rows = (matching.length === 0 ? applicable : matching).map(template => this.row(template, keyed(template) || matching.length === 0 ? template.key : word, grouped(template), {
|
|
127
|
+
start,
|
|
128
|
+
end: caret
|
|
129
|
+
}, () => {
|
|
130
|
+
const range = selected ? selection : {
|
|
131
|
+
start,
|
|
132
|
+
end: editor.getCaret()
|
|
133
|
+
};
|
|
134
|
+
void this.run(template, [{
|
|
135
|
+
range,
|
|
136
|
+
selectedText: selected ? dedentLines(text) : undefined
|
|
137
|
+
}]);
|
|
138
|
+
}));
|
|
139
|
+
completion.showItems(rows, start);
|
|
140
|
+
}
|
|
141
|
+
/* Hands the selection to the host as a draft of a template; false without a selection or without a host that saves. */
|
|
142
|
+
saveSelection() {
|
|
143
|
+
const {
|
|
144
|
+
editor,
|
|
145
|
+
project
|
|
146
|
+
} = this.language;
|
|
147
|
+
const selection = editor.getSelection();
|
|
148
|
+
if (comparePositions(selection.start, selection.end) === 0) {
|
|
149
|
+
editor.announce(project.i18n.t('editor:language.templates.selectToSave'));
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (project.host.saveAsLiveTemplate === undefined) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const context = templateContextAt(this.language, selection.start);
|
|
156
|
+
project.host.saveAsLiveTemplate({
|
|
157
|
+
body: escapeSnippetText(dedentLines(editor.textInRange(selection))),
|
|
158
|
+
languages: [context.families[0]],
|
|
159
|
+
places: [context.place]
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/* The keys that compete with a live template for Tab: an Emmet abbreviation, then a postfix key. */
|
|
164
|
+
customKey() {
|
|
165
|
+
const abbreviation = this.language.markup.abbreviationAt();
|
|
166
|
+
return abbreviation === null ? this.postfix.key() : {
|
|
167
|
+
start: abbreviation,
|
|
168
|
+
expand: null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/* The templates every caret has the same key of, ending at the caret, in a context they apply in. */
|
|
172
|
+
matchAtCarets(carets) {
|
|
173
|
+
const matches = carets.map(caret => this.matchAt(caret));
|
|
174
|
+
const primary = matches.at(-1);
|
|
175
|
+
if (primary === null || primary === undefined) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const templates = primary.templates.filter(template => matches.every(match => match?.templates.some(other => other.id === template.id) === true));
|
|
179
|
+
return templates.length === 0 ? null : {
|
|
180
|
+
start: primary.start,
|
|
181
|
+
templates
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/*
|
|
185
|
+
* The templates whose key is the longest one that ends at the caret, as IntelliJ looks them up: when the longest
|
|
186
|
+
* key found applies nowhere, no shorter key is tried.
|
|
187
|
+
*/
|
|
188
|
+
matchAt(caret) {
|
|
189
|
+
const {
|
|
190
|
+
editor,
|
|
191
|
+
project
|
|
192
|
+
} = this.language;
|
|
193
|
+
const {
|
|
194
|
+
live
|
|
195
|
+
} = project.templates;
|
|
196
|
+
const before = textBeforeOf(editor, caret);
|
|
197
|
+
const longest = live.reduce((length, template) => Math.max(length, template.key.length), 0);
|
|
198
|
+
for (let length = Math.min(longest, before.length); length >= 1; length--) {
|
|
199
|
+
const index = before.length - length;
|
|
200
|
+
const key = before.slice(index);
|
|
201
|
+
const found = live.filter(template => template.key === key);
|
|
202
|
+
if (found.length === 0 || startsInsideWord(before, index)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const start = {
|
|
206
|
+
line: caret.line,
|
|
207
|
+
character: index
|
|
208
|
+
};
|
|
209
|
+
const context = templateContextAt(this.language, start);
|
|
210
|
+
const templates = found.filter(template => templateApplies(template, context));
|
|
211
|
+
return templates.length === 0 ? null : {
|
|
212
|
+
start,
|
|
213
|
+
templates
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
/* Replaces the text between `start` and the caret, as far as the primary caret is from it, at every caret. */
|
|
219
|
+
expandAtCarets(template, start) {
|
|
220
|
+
const {
|
|
221
|
+
editor
|
|
222
|
+
} = this.language;
|
|
223
|
+
const length = Math.max(0, editor.getCaret().character - start.character);
|
|
224
|
+
void this.run(template, editor.getSelections().map(selection => ({
|
|
225
|
+
range: {
|
|
226
|
+
start: {
|
|
227
|
+
line: selection.end.line,
|
|
228
|
+
character: Math.max(0, selection.end.character - length)
|
|
229
|
+
},
|
|
230
|
+
end: selection.end
|
|
231
|
+
}
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
234
|
+
run(template, placements) {
|
|
235
|
+
return insertSnippet(this.language, bodyWithIndentUnit(template.body, indentUnitOf(this.language.editor)), placements);
|
|
236
|
+
}
|
|
237
|
+
/* The rows for the word before the caret, each a template whose key starts with it. */
|
|
238
|
+
rows(position) {
|
|
239
|
+
const {
|
|
240
|
+
editor,
|
|
241
|
+
project
|
|
242
|
+
} = this.language;
|
|
243
|
+
if (!project.templates.settings.liveInCompletion) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
const before = textBeforeOf(editor, position);
|
|
247
|
+
if (identifierPrefix(before) === '') {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
const contexts = new Map();
|
|
251
|
+
return project.templates.live.flatMap(template => {
|
|
252
|
+
const length = prefixLength(template.key, before);
|
|
253
|
+
if (length === 0) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const start = {
|
|
257
|
+
line: position.line,
|
|
258
|
+
character: position.character - length
|
|
259
|
+
};
|
|
260
|
+
const context = contexts.get(length) ?? templateContextAt(this.language, start);
|
|
261
|
+
contexts.set(length, context);
|
|
262
|
+
return templateApplies(template, context) ? [this.row(template, template.key, sortAfterServer(template.key), {
|
|
263
|
+
start,
|
|
264
|
+
end: position
|
|
265
|
+
}, () => this.expandAtCarets(template, start))] : [];
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
row(template, filterText, sortText, range, accept) {
|
|
269
|
+
const item = {
|
|
270
|
+
label: template.key,
|
|
271
|
+
kind: SNIPPET_KIND,
|
|
272
|
+
detail: template.description,
|
|
273
|
+
documentation: {
|
|
274
|
+
kind: 'markdown',
|
|
275
|
+
value: `\`\`\`\n${template.body}\n\`\`\``
|
|
276
|
+
},
|
|
277
|
+
filterText,
|
|
278
|
+
sortText,
|
|
279
|
+
textEdit: {
|
|
280
|
+
range,
|
|
281
|
+
newText: template.key
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
return {
|
|
285
|
+
item,
|
|
286
|
+
accept
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
package/dist/locales/en.json
CHANGED
|
@@ -219,6 +219,36 @@
|
|
|
219
219
|
"title": "Choose Content to Paste",
|
|
220
220
|
"empty": "Nothing was copied yet"
|
|
221
221
|
},
|
|
222
|
+
"templates": {
|
|
223
|
+
"none": "No templates defined in this context",
|
|
224
|
+
"selectToSave": "Select the text to save as a live template"
|
|
225
|
+
},
|
|
226
|
+
"postfix": {
|
|
227
|
+
"expressions": "Expressions"
|
|
228
|
+
},
|
|
229
|
+
"surround": {
|
|
230
|
+
"title": "Surround With",
|
|
231
|
+
"selectTemplate": "Select Template",
|
|
232
|
+
"liveTemplates": "Live templates",
|
|
233
|
+
"configure": "Configure live templates…",
|
|
234
|
+
"comments": "{{construct}} Comments",
|
|
235
|
+
"none": "Couldn't find Surround With variants applicable to the current context",
|
|
236
|
+
"noTemplates": "No surround templates defined in this context",
|
|
237
|
+
"unwrap": {
|
|
238
|
+
"title": "Choose the statement to unwrap/remove",
|
|
239
|
+
"if": "Unwrap 'if...'",
|
|
240
|
+
"else": "Unwrap 'else...'",
|
|
241
|
+
"removeElse": "Remove 'else...'",
|
|
242
|
+
"for": "Unwrap 'for...'",
|
|
243
|
+
"while": "Unwrap 'while...'",
|
|
244
|
+
"do": "Unwrap 'do...'",
|
|
245
|
+
"try": "Unwrap 'try...'",
|
|
246
|
+
"removeCatch": "Remove 'catch...'",
|
|
247
|
+
"removeFinally": "Remove 'finally...'",
|
|
248
|
+
"braces": "Unwrap braces",
|
|
249
|
+
"tag": "Remove enclosing tag: {{name}}"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
222
252
|
"announce": {
|
|
223
253
|
"problem": "{{severity}}: {{message}}",
|
|
224
254
|
"severity": {
|
package/dist/locales/nl.json
CHANGED
|
@@ -219,6 +219,36 @@
|
|
|
219
219
|
"title": "Kies wat je plakt",
|
|
220
220
|
"empty": "Er is nog niets gekopieerd"
|
|
221
221
|
},
|
|
222
|
+
"templates": {
|
|
223
|
+
"none": "Geen sjablonen gedefinieerd in deze context",
|
|
224
|
+
"selectToSave": "Selecteer de tekst die je als live sjabloon wilt opslaan"
|
|
225
|
+
},
|
|
226
|
+
"postfix": {
|
|
227
|
+
"expressions": "Expressies"
|
|
228
|
+
},
|
|
229
|
+
"surround": {
|
|
230
|
+
"title": "Omring met",
|
|
231
|
+
"selectTemplate": "Kies een sjabloon",
|
|
232
|
+
"liveTemplates": "Live sjablonen",
|
|
233
|
+
"configure": "Live sjablonen instellen…",
|
|
234
|
+
"comments": "{{construct}}-commentaar",
|
|
235
|
+
"none": "Geen Omring met-varianten gevonden voor de huidige context",
|
|
236
|
+
"noTemplates": "Geen omringende sjablonen gedefinieerd in deze context",
|
|
237
|
+
"unwrap": {
|
|
238
|
+
"title": "Kies het statement om uit te pakken of te verwijderen",
|
|
239
|
+
"if": "'if...' uitpakken",
|
|
240
|
+
"else": "'else...' uitpakken",
|
|
241
|
+
"removeElse": "'else...' verwijderen",
|
|
242
|
+
"for": "'for...' uitpakken",
|
|
243
|
+
"while": "'while...' uitpakken",
|
|
244
|
+
"do": "'do...' uitpakken",
|
|
245
|
+
"try": "'try...' uitpakken",
|
|
246
|
+
"removeCatch": "'catch...' verwijderen",
|
|
247
|
+
"removeFinally": "'finally...' verwijderen",
|
|
248
|
+
"braces": "Accolades uitpakken",
|
|
249
|
+
"tag": "Omringende tag verwijderen: {{name}}"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
222
252
|
"announce": {
|
|
223
253
|
"problem": "{{severity}}: {{message}}",
|
|
224
254
|
"severity": {
|
package/dist/markup-context.d.ts
CHANGED
|
@@ -30,8 +30,26 @@ export interface TagPair {
|
|
|
30
30
|
readonly to: number;
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
export interface TagSpan {
|
|
34
|
+
readonly from: number;
|
|
35
|
+
readonly to: number;
|
|
36
|
+
}
|
|
37
|
+
export interface EnclosingElement {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly open: TagSpan;
|
|
40
|
+
readonly close: TagSpan | null;
|
|
41
|
+
}
|
|
33
42
|
export declare const MAX_MARKUP_SCAN = 1000000;
|
|
34
43
|
export declare function markupSyntaxOf(languageId: string): MarkupSyntax | null;
|
|
35
44
|
export declare function isVoidElement(name: string, syntax: MarkupSyntax): boolean;
|
|
45
|
+
export declare function enclosingElements(text: string, offset: number, syntax: MarkupSyntax): EnclosingElement[];
|
|
46
|
+
export interface TextReplacement {
|
|
47
|
+
readonly from: number;
|
|
48
|
+
readonly to: number;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function lineStartOf(text: string, offset: number): number;
|
|
52
|
+
export declare function lineEndOf(text: string, offset: number): number;
|
|
53
|
+
export declare function removeTagEdits(text: string, element: EnclosingElement, unit: string): TextReplacement[];
|
|
36
54
|
export declare function markupPlaceAt(text: string, offset: number, syntax: MarkupSyntax): MarkupPlace | null;
|
|
37
55
|
export declare function tagPairAt(text: string, offset: number, syntax: MarkupSyntax): TagPair | null;
|
package/dist/markup-context.js
CHANGED
|
@@ -37,6 +37,7 @@ class Stop {
|
|
|
37
37
|
*/
|
|
38
38
|
class MarkupScanner {
|
|
39
39
|
pairs = [];
|
|
40
|
+
elements = [];
|
|
40
41
|
text;
|
|
41
42
|
syntax;
|
|
42
43
|
until;
|
|
@@ -131,7 +132,10 @@ class MarkupScanner {
|
|
|
131
132
|
const name = text.slice(nameFrom, nameTo);
|
|
132
133
|
const end = this.tagEnd(at, closing, name, nameFrom, nameTo);
|
|
133
134
|
if (closing) {
|
|
134
|
-
this.close(name, nameFrom, nameTo
|
|
135
|
+
this.close(name, nameFrom, nameTo, {
|
|
136
|
+
from: at,
|
|
137
|
+
to: end
|
|
138
|
+
});
|
|
135
139
|
return end;
|
|
136
140
|
}
|
|
137
141
|
const selfClosing = text[end - 2] === '/' && text[end - 1] === '>';
|
|
@@ -139,12 +143,19 @@ class MarkupScanner {
|
|
|
139
143
|
return end;
|
|
140
144
|
}
|
|
141
145
|
if (this.syntax !== 'xml' && RAW_ELEMENTS.has(name.toLowerCase())) {
|
|
142
|
-
return this.rawContent(
|
|
146
|
+
return this.rawContent({
|
|
147
|
+
from: at,
|
|
148
|
+
to: end
|
|
149
|
+
}, name, nameFrom, nameTo);
|
|
143
150
|
}
|
|
144
151
|
this.open.push({
|
|
145
152
|
name,
|
|
146
153
|
nameFrom,
|
|
147
|
-
nameTo
|
|
154
|
+
nameTo,
|
|
155
|
+
tag: {
|
|
156
|
+
from: at,
|
|
157
|
+
to: end
|
|
158
|
+
}
|
|
148
159
|
});
|
|
149
160
|
return end;
|
|
150
161
|
}
|
|
@@ -224,7 +235,8 @@ class MarkupScanner {
|
|
|
224
235
|
return text.length;
|
|
225
236
|
}
|
|
226
237
|
/* The content of `script`, `style` and the like is not markup, up to the end tag of the element. */
|
|
227
|
-
rawContent(
|
|
238
|
+
rawContent(tag, name, nameFrom, nameTo) {
|
|
239
|
+
const from = tag.to;
|
|
228
240
|
const pattern = new RegExp(`</${name}(?=[\\s/>]|$)`, 'gi');
|
|
229
241
|
pattern.lastIndex = from;
|
|
230
242
|
const match = pattern.exec(this.text);
|
|
@@ -248,10 +260,18 @@ class MarkupScanner {
|
|
|
248
260
|
to: closeTo
|
|
249
261
|
}
|
|
250
262
|
});
|
|
263
|
+
this.elements.push({
|
|
264
|
+
name,
|
|
265
|
+
open: tag,
|
|
266
|
+
close: {
|
|
267
|
+
from: end,
|
|
268
|
+
to: after
|
|
269
|
+
}
|
|
270
|
+
});
|
|
251
271
|
return after;
|
|
252
272
|
}
|
|
253
273
|
/* An end tag closes the nearest open element of its name and every element left open inside it. */
|
|
254
|
-
close(name, nameFrom, nameTo) {
|
|
274
|
+
close(name, nameFrom, nameTo, tag) {
|
|
255
275
|
const caseless = this.syntax !== 'xml';
|
|
256
276
|
for (let index = this.open.length - 1; index >= 0; index--) {
|
|
257
277
|
const element = this.open[index];
|
|
@@ -266,11 +286,108 @@ class MarkupScanner {
|
|
|
266
286
|
to: nameTo
|
|
267
287
|
}
|
|
268
288
|
});
|
|
289
|
+
this.elements.push({
|
|
290
|
+
name: element.name,
|
|
291
|
+
open: element.tag,
|
|
292
|
+
close: tag
|
|
293
|
+
});
|
|
269
294
|
this.open.length = index;
|
|
270
295
|
return;
|
|
271
296
|
}
|
|
272
297
|
}
|
|
273
298
|
}
|
|
299
|
+
/* The elements still open at the end of the text. */
|
|
300
|
+
unclosed() {
|
|
301
|
+
return this.open.map(element => ({
|
|
302
|
+
name: element.name,
|
|
303
|
+
open: element.tag,
|
|
304
|
+
close: null
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/*
|
|
309
|
+
* The elements whose tags hold `offset`, innermost first: each with its start tag and its end tag, which an element left
|
|
310
|
+
* open does not have. Empty for a document too large to read on a keystroke.
|
|
311
|
+
*/
|
|
312
|
+
export function enclosingElements(text, offset, syntax) {
|
|
313
|
+
if (text.length > MAX_MARKUP_SCAN) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const scanner = new MarkupScanner(text, syntax, Infinity);
|
|
317
|
+
scanner.run();
|
|
318
|
+
return [...scanner.elements, ...scanner.unclosed()].filter(element => element.open.from <= offset && offset <= (element.close?.to ?? text.length)).sort((left, right) => right.open.from - left.open.from);
|
|
319
|
+
}
|
|
320
|
+
export function lineStartOf(text, offset) {
|
|
321
|
+
let at = offset;
|
|
322
|
+
while (at > 0 && text[at - 1] !== '\n' && text[at - 1] !== '\r') {
|
|
323
|
+
at--;
|
|
324
|
+
}
|
|
325
|
+
return at;
|
|
326
|
+
}
|
|
327
|
+
export function lineEndOf(text, offset) {
|
|
328
|
+
let at = offset;
|
|
329
|
+
while (at < text.length && text[at] !== '\n' && text[at] !== '\r') {
|
|
330
|
+
at++;
|
|
331
|
+
}
|
|
332
|
+
return at;
|
|
333
|
+
}
|
|
334
|
+
function breakLengthAt(text, offset) {
|
|
335
|
+
if (text.startsWith('\r\n', offset)) {
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
return text[offset] === '\n' || text[offset] === '\r' ? 1 : 0;
|
|
339
|
+
}
|
|
340
|
+
/* The tag alone on its line takes the line with it, and the break before it where no break follows. */
|
|
341
|
+
function tagReplacement(text, tag) {
|
|
342
|
+
const start = lineStartOf(text, tag.from);
|
|
343
|
+
const end = lineEndOf(text, tag.to);
|
|
344
|
+
if (text.slice(start, tag.from).trim() !== '' || text.slice(tag.to, end).trim() !== '') {
|
|
345
|
+
return {
|
|
346
|
+
from: tag.from,
|
|
347
|
+
to: tag.to,
|
|
348
|
+
text: ''
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const after = breakLengthAt(text, end);
|
|
352
|
+
const before = start > 0 && after === 0 ? text.startsWith('\r\n', start - 2) ? 2 : 1 : 0;
|
|
353
|
+
return {
|
|
354
|
+
from: start - before,
|
|
355
|
+
to: end + after,
|
|
356
|
+
text: ''
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/*
|
|
360
|
+
* The edits that take an element's tags away and keep what is between them: each tag that stands alone on its line
|
|
361
|
+
* goes with its line, and when the start tag does, the lines it enclosed lose one indentation unit. The offsets are
|
|
362
|
+
* those of `text`, ascending and apart.
|
|
363
|
+
*/
|
|
364
|
+
export function removeTagEdits(text, element, unit) {
|
|
365
|
+
const open = tagReplacement(text, element.open);
|
|
366
|
+
const edits = [open];
|
|
367
|
+
if (element.close === null) {
|
|
368
|
+
return edits;
|
|
369
|
+
}
|
|
370
|
+
const close = tagReplacement(text, element.close);
|
|
371
|
+
const openAlone = open.from !== element.open.from || open.to !== element.open.to;
|
|
372
|
+
const closeAlone = close.from !== element.close.from || close.to !== element.close.to;
|
|
373
|
+
const indent = unit === '\t' ? /^\t/ : new RegExp(`^ {1,${unit.length}}`);
|
|
374
|
+
const lastLine = lineStartOf(text, element.close.from);
|
|
375
|
+
let line = lineEndOf(text, element.open.to);
|
|
376
|
+
line += breakLengthAt(text, line);
|
|
377
|
+
while (openAlone && line <= lastLine && line < text.length && !(closeAlone && line === lastLine)) {
|
|
378
|
+
const found = indent.exec(text.slice(line, lineEndOf(text, line)));
|
|
379
|
+
if (found !== null) {
|
|
380
|
+
edits.push({
|
|
381
|
+
from: line,
|
|
382
|
+
to: line + found[0].length,
|
|
383
|
+
text: ''
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
line = lineEndOf(text, line);
|
|
387
|
+
line += breakLengthAt(text, line);
|
|
388
|
+
}
|
|
389
|
+
edits.push(close);
|
|
390
|
+
return edits;
|
|
274
391
|
}
|
|
275
392
|
/* What `offset` is in; null for a document too large to read on a keystroke. */
|
|
276
393
|
export function markupPlaceAt(text, offset, syntax) {
|
package/dist/markup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EditorPosition } from '@adecore/editor';
|
|
1
2
|
import type { EditorLanguage } from './editor-language.ts';
|
|
2
3
|
export interface MarkupSettings {
|
|
3
4
|
readonly emmet?: boolean;
|
|
@@ -16,6 +17,8 @@ export declare class MarkupFeature {
|
|
|
16
17
|
get supported(): boolean;
|
|
17
18
|
configure(settings: MarkupSettings): void;
|
|
18
19
|
expand(): boolean;
|
|
20
|
+
abbreviationAt(): EditorPosition | null;
|
|
21
|
+
private abbreviation;
|
|
19
22
|
startWrap(): boolean;
|
|
20
23
|
submitWrap(abbreviation: string): boolean;
|
|
21
24
|
cancelWrap(): void;
|
package/dist/markup.js
CHANGED
|
@@ -60,6 +60,14 @@ export class MarkupFeature {
|
|
|
60
60
|
}
|
|
61
61
|
/* Expands the abbreviation before the caret, its places to fill as a snippet; false when there is none, which leaves Tab its own meaning. */
|
|
62
62
|
expand() {
|
|
63
|
+
const found = this.abbreviation();
|
|
64
|
+
return found !== null && this.insert(found.range, found.expanded, found.line);
|
|
65
|
+
}
|
|
66
|
+
/* Where the abbreviation that `expand` would take starts, without expanding it; null under the conditions where `expand` does nothing. */
|
|
67
|
+
abbreviationAt() {
|
|
68
|
+
return this.abbreviation()?.range.start ?? null;
|
|
69
|
+
}
|
|
70
|
+
abbreviation() {
|
|
63
71
|
const {
|
|
64
72
|
syntax
|
|
65
73
|
} = this;
|
|
@@ -67,13 +75,13 @@ export class MarkupFeature {
|
|
|
67
75
|
editor
|
|
68
76
|
} = this.language;
|
|
69
77
|
if (syntax === null || !this.settings.emmet || !this.editable()) {
|
|
70
|
-
return
|
|
78
|
+
return null;
|
|
71
79
|
}
|
|
72
80
|
const caret = editor.getCaret();
|
|
73
81
|
const line = lineTextOf(editor, caret.line);
|
|
74
82
|
const found = abbreviationBefore(line, caret.character);
|
|
75
83
|
if (found === null) {
|
|
76
|
-
return
|
|
84
|
+
return null;
|
|
77
85
|
}
|
|
78
86
|
const range = {
|
|
79
87
|
start: {
|
|
@@ -86,13 +94,17 @@ export class MarkupFeature {
|
|
|
86
94
|
}
|
|
87
95
|
};
|
|
88
96
|
if (markupPlaceAt(editor.getText(), editor.offsetAt(range.start), syntax)?.kind !== 'text') {
|
|
89
|
-
return
|
|
97
|
+
return null;
|
|
90
98
|
}
|
|
91
99
|
const expanded = expandAbbreviation(found.abbreviation, {
|
|
92
100
|
syntax,
|
|
93
101
|
indent: this.indent()
|
|
94
102
|
});
|
|
95
|
-
return expanded
|
|
103
|
+
return expanded === null ? null : {
|
|
104
|
+
range,
|
|
105
|
+
expanded,
|
|
106
|
+
line
|
|
107
|
+
};
|
|
96
108
|
}
|
|
97
109
|
/* Opens the field for an abbreviation to wrap the selection in, or the text of the caret's line without one. */
|
|
98
110
|
startWrap() {
|
package/dist/pick.d.ts
CHANGED
package/dist/pick.js
CHANGED
|
@@ -260,9 +260,19 @@ export class PickFeature {
|
|
|
260
260
|
this.requery(this.query + event.key);
|
|
261
261
|
return true;
|
|
262
262
|
}
|
|
263
|
-
return
|
|
263
|
+
return spec.filter !== true && event.key.length === 1 && this.takeMnemonic(event.key);
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
/* Takes the visible row whose mnemonic is the typed character, whatever the case. */
|
|
267
|
+
takeMnemonic(character) {
|
|
268
|
+
const wanted = character.toLowerCase();
|
|
269
|
+
const row = this.visibleGroups().flatMap(group => group.rows).find(candidate => candidate.mnemonic?.toLowerCase() === wanted);
|
|
270
|
+
if (row === undefined) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
this.choose(row.id);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
266
276
|
schedulePreview() {
|
|
267
277
|
const spec = this.spec;
|
|
268
278
|
this.timers.clear(this.previewTimer);
|