@adecore/editor-react 0.42.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.
Files changed (150) hide show
  1. package/dist/CallHierarchyPanel.js +177 -181
  2. package/dist/EditorContextMenu.js +118 -105
  3. package/dist/LanguagePopups.js +7 -0
  4. package/dist/PickPopup.js +245 -104
  5. package/dist/WrapCard.d.ts +8 -0
  6. package/dist/WrapCard.js +172 -0
  7. package/dist/call-hierarchy-model.d.ts +4 -2
  8. package/dist/call-hierarchy-model.js +20 -0
  9. package/dist/call-hierarchy.d.ts +3 -0
  10. package/dist/call-hierarchy.js +54 -22
  11. package/dist/clipboard-history.d.ts +31 -0
  12. package/dist/clipboard-history.js +70 -0
  13. package/dist/code-actions.d.ts +3 -1
  14. package/dist/code-actions.js +50 -10
  15. package/dist/code-vision.js +2 -3
  16. package/dist/completion.d.ts +28 -0
  17. package/dist/completion.js +83 -27
  18. package/dist/context-menu-rows.d.ts +1 -1
  19. package/dist/context-menu-rows.js +4 -0
  20. package/dist/definition-link.js +15 -5
  21. package/dist/document-colors.d.ts +24 -0
  22. package/dist/document-colors.js +194 -0
  23. package/dist/document-links.d.ts +22 -0
  24. package/dist/document-links.js +133 -0
  25. package/dist/document-query.d.ts +15 -0
  26. package/dist/document-query.js +54 -0
  27. package/dist/edit-locations.d.ts +20 -0
  28. package/dist/edit-locations.js +136 -0
  29. package/dist/editor-language.d.ts +23 -1
  30. package/dist/editor-language.js +83 -2
  31. package/dist/emmet.d.ts +13 -0
  32. package/dist/emmet.js +62 -0
  33. package/dist/folding.js +2 -3
  34. package/dist/format-on-type.js +3 -5
  35. package/dist/highlights.js +2 -3
  36. package/dist/history.d.ts +7 -0
  37. package/dist/history.js +79 -1
  38. package/dist/host-types.d.ts +10 -0
  39. package/dist/index.d.ts +13 -2
  40. package/dist/index.js +10 -0
  41. package/dist/inlay-hints.js +2 -2
  42. package/dist/inline-completion.d.ts +39 -0
  43. package/dist/inline-completion.js +221 -0
  44. package/dist/key-priority.d.ts +9 -5
  45. package/dist/key-priority.js +11 -6
  46. package/dist/linked-editing.d.ts +26 -0
  47. package/dist/linked-editing.js +215 -0
  48. package/dist/live-templates.d.ts +16 -0
  49. package/dist/live-templates.js +289 -0
  50. package/dist/locales/en.json +61 -2
  51. package/dist/locales/nl.json +61 -2
  52. package/dist/markup-context.d.ts +55 -0
  53. package/dist/markup-context.js +408 -0
  54. package/dist/markup.d.ts +39 -0
  55. package/dist/markup.js +465 -0
  56. package/dist/navigation.js +4 -6
  57. package/dist/paste-history.d.ts +7 -0
  58. package/dist/paste-history.js +104 -0
  59. package/dist/peek.js +3 -4
  60. package/dist/pick.d.ts +21 -2
  61. package/dist/pick.js +152 -19
  62. package/dist/popups.d.ts +10 -0
  63. package/dist/popups.js +2 -1
  64. package/dist/postfix.d.ts +21 -0
  65. package/dist/postfix.js +212 -0
  66. package/dist/project-language.d.ts +20 -1
  67. package/dist/project-language.js +132 -18
  68. package/dist/rename.js +8 -11
  69. package/dist/rendered-docs.js +1 -2
  70. package/dist/selection-ranges.js +2 -3
  71. package/dist/shortcut-keys.d.ts +24 -0
  72. package/dist/signature.js +3 -6
  73. package/dist/snippet-insert.d.ts +10 -0
  74. package/dist/snippet-insert.js +217 -0
  75. package/dist/snippet-session.d.ts +2 -2
  76. package/dist/snippet-session.js +3 -3
  77. package/dist/snippet.d.ts +7 -1
  78. package/dist/snippet.js +65 -8
  79. package/dist/surround-constructs.d.ts +10 -0
  80. package/dist/surround-constructs.js +100 -0
  81. package/dist/surround.d.ts +18 -0
  82. package/dist/surround.js +399 -0
  83. package/dist/symbol-picker.js +4 -6
  84. package/dist/symbols.js +22 -3
  85. package/dist/template-builtins.d.ts +3 -0
  86. package/dist/template-builtins.js +46 -0
  87. package/dist/template-context.d.ts +12 -0
  88. package/dist/template-context.js +56 -0
  89. package/dist/templates.d.ts +59 -0
  90. package/dist/templates.js +103 -0
  91. package/dist/testing.d.ts +23 -1
  92. package/dist/testing.js +114 -0
  93. package/package.json +7 -6
  94. package/src/CallHierarchyPanel.tsx +26 -11
  95. package/src/EditorContextMenu.tsx +7 -0
  96. package/src/LanguagePopups.tsx +4 -0
  97. package/src/PickPopup.tsx +47 -5
  98. package/src/WrapCard.tsx +64 -0
  99. package/src/call-hierarchy-model.ts +25 -3
  100. package/src/call-hierarchy.ts +62 -20
  101. package/src/clipboard-history.ts +102 -0
  102. package/src/code-actions.ts +48 -11
  103. package/src/code-vision.ts +3 -3
  104. package/src/completion.ts +123 -25
  105. package/src/context-menu-rows.ts +3 -2
  106. package/src/definition-link.ts +17 -5
  107. package/src/document-colors.ts +189 -0
  108. package/src/document-links.ts +129 -0
  109. package/src/document-query.ts +71 -0
  110. package/src/edit-locations.ts +162 -0
  111. package/src/editor-language.ts +86 -3
  112. package/src/emmet-module.d.ts +44 -0
  113. package/src/emmet.ts +73 -0
  114. package/src/folding.ts +3 -3
  115. package/src/format-on-type.ts +5 -5
  116. package/src/highlights.ts +3 -3
  117. package/src/history.ts +81 -1
  118. package/src/host-types.ts +17 -0
  119. package/src/index.ts +28 -3
  120. package/src/inlay-hints.ts +2 -2
  121. package/src/inline-completion.ts +221 -0
  122. package/src/key-priority.ts +11 -6
  123. package/src/linked-editing.ts +199 -0
  124. package/src/live-templates.ts +315 -0
  125. package/src/locales/en.json +61 -2
  126. package/src/locales/nl.json +61 -2
  127. package/src/markup-context.ts +387 -0
  128. package/src/markup.ts +377 -0
  129. package/src/navigation.ts +6 -6
  130. package/src/paste-history.ts +111 -0
  131. package/src/peek.ts +4 -4
  132. package/src/pick.ts +218 -20
  133. package/src/popups.ts +21 -1
  134. package/src/postfix.ts +224 -0
  135. package/src/project-language.ts +139 -19
  136. package/src/rename.ts +11 -11
  137. package/src/rendered-docs.ts +2 -2
  138. package/src/selection-ranges.ts +3 -3
  139. package/src/signature.ts +6 -6
  140. package/src/snippet-insert.ts +258 -0
  141. package/src/snippet-session.ts +9 -4
  142. package/src/snippet.ts +82 -7
  143. package/src/surround-constructs.ts +85 -0
  144. package/src/surround.ts +430 -0
  145. package/src/symbol-picker.ts +6 -6
  146. package/src/symbols.ts +30 -4
  147. package/src/template-builtins.ts +94 -0
  148. package/src/template-context.ts +65 -0
  149. package/src/templates.ts +197 -0
  150. package/src/testing.ts +126 -1
@@ -0,0 +1,399 @@
1
+ import { commentSyntax } from '@adecore/editor';
2
+ import { comparePositions, shiftRange } from './diagnostics-model.js';
3
+ import { KEY_PRIORITY } from './key-priority.js';
4
+ import { lineTextOf, textBeforeOf } from './line-text.js';
5
+ import { enclosingElements, lineEndOf, lineStartOf, markupSyntaxOf, removeTagEdits } from './markup-context.js';
6
+ import { isShortcut } from './shortcut-keys.js';
7
+ import { bodyWithIndentUnit, dedentLines, insertSnippet } from './snippet-insert.js';
8
+ import { indentSnippet } from './snippet-indent.js';
9
+ import { compileSnippet, snippetVariables } from './snippet.js';
10
+ import { expressionConstructs, regionConstructs, statementConstructs } from './surround-constructs.js';
11
+ import { templateApplies, templateContextAt } from './template-context.js';
12
+ const TOAST_ID = 'language-surround';
13
+ const SELECTED_TEXT = 'TM_SELECTED_TEXT';
14
+ const MNEMONICS = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ';
15
+ const LINE_BREAK = /\r\n|\r|\n/;
16
+ /* Where a mnemonic can come from: the first character of a name that no row has taken. */
17
+ function mnemonicOf(name, taken) {
18
+ const found = [...name].find(character => /[\p{L}\p{N}]/u.test(character) && !taken.has(character.toUpperCase()));
19
+ if (found === undefined) {
20
+ return undefined;
21
+ }
22
+ taken.add(found.toUpperCase());
23
+ return found.toUpperCase();
24
+ }
25
+ /* Overlapping ranges become one, the one holding the primary range last. Null where two ranges share a line. */
26
+ function mergedRanges(ranges) {
27
+ const primary = ranges.at(-1);
28
+ const sorted = [...ranges].sort((left, right) => comparePositions(left.start, right.start));
29
+ const merged = [];
30
+ for (const range of sorted) {
31
+ const last = merged.at(-1);
32
+ if (last !== undefined && comparePositions(range.start, last.end) <= 0) {
33
+ merged[merged.length - 1] = {
34
+ start: last.start,
35
+ end: comparePositions(range.end, last.end) > 0 ? range.end : last.end
36
+ };
37
+ } else {
38
+ merged.push(range);
39
+ }
40
+ }
41
+ if (merged.some((range, index) => index > 0 && range.start.line <= merged[index - 1].end.line)) {
42
+ return null;
43
+ }
44
+ const index = merged.findIndex(range => comparePositions(range.start, primary.start) <= 0 && comparePositions(primary.start, range.end) <= 0);
45
+ return [...merged.filter((_range, other) => other !== index), merged[index]];
46
+ }
47
+ /*
48
+ * Surround With, Surround with Live Template and Unwrap/Remove, as IntelliJ's handlers offer them. Surround With
49
+ * wraps the selection, or the content of the caret's line, in a statement, an expression, a region comment or a live
50
+ * template that uses the selected text; Unwrap/Remove takes the construct around the caret away and keeps what is in it.
51
+ * Each is one edit, so one undo step, and nothing happens in a read-only editor.
52
+ */
53
+ export class SurroundFeature {
54
+ language;
55
+ constructor(language) {
56
+ this.language = language;
57
+ language.onDispose(language.onKey(event => {
58
+ if (isShortcut(language.shortcuts.surroundWith, event)) {
59
+ return this.open();
60
+ }
61
+ if (isShortcut(language.shortcuts.surroundWithLiveTemplate, event)) {
62
+ return this.openTemplates();
63
+ }
64
+ return isShortcut(language.shortcuts.unwrap, event) && this.unwrap();
65
+ }, KEY_PRIORITY.templates));
66
+ }
67
+ /* Opens Surround With under the selection. True, since the chord is taken, also where nothing applies. */
68
+ open() {
69
+ return this.show(false);
70
+ }
71
+ /* Opens Surround with Live Template: Emmet in markup, then the templates that use the selected text. */
72
+ openTemplates() {
73
+ return this.show(true);
74
+ }
75
+ /* Opens the list of constructs around the caret that can be unwrapped or removed. */
76
+ unwrap() {
77
+ const {
78
+ editor,
79
+ pick,
80
+ project
81
+ } = this.language;
82
+ if (editor.isReadOnly?.() === true) {
83
+ return true;
84
+ }
85
+ const rows = this.unwrapRows();
86
+ if (rows.length === 0) {
87
+ return true;
88
+ }
89
+ const text = editor.getText();
90
+ pick.open({
91
+ anchor: editor.getCaret(),
92
+ title: project.i18n.t('editor:language.surround.unwrap.title'),
93
+ groups: [{
94
+ title: null,
95
+ rows: rows.map((row, index) => ({
96
+ id: String(index),
97
+ label: row.label,
98
+ detail: ''
99
+ }))
100
+ }],
101
+ filter: true,
102
+ accept: id => this.applyUnwrap(rows[Number(id)]),
103
+ preview: id => Promise.resolve(this.unwrapPreview(text, rows[Number(id)]))
104
+ });
105
+ return true;
106
+ }
107
+ show(templatesOnly) {
108
+ const {
109
+ editor,
110
+ pick,
111
+ project
112
+ } = this.language;
113
+ if (editor.isReadOnly?.() === true) {
114
+ return true;
115
+ }
116
+ const subject = this.subjectOf();
117
+ if (subject === null) {
118
+ this.refuse('editor:language.surround.none');
119
+ return true;
120
+ }
121
+ const groups = this.groupsOf(subject, templatesOnly);
122
+ const entries = new Map(groups.flatMap(group => group.entries).map(entry => [entry.row.id, entry]));
123
+ if (entries.size === 0) {
124
+ this.refuse(templatesOnly ? 'editor:language.surround.noTemplates' : 'editor:language.surround.none');
125
+ return true;
126
+ }
127
+ const primary = subject.placements.at(-1);
128
+ pick.open({
129
+ anchor: primary.range.start,
130
+ title: project.i18n.t(templatesOnly ? 'editor:language.surround.selectTemplate' : 'editor:language.surround.title'),
131
+ groups: groups.map(group => ({
132
+ title: group.title,
133
+ rows: group.entries.map(entry => entry.row)
134
+ })),
135
+ accept: id => entries.get(id).run(),
136
+ preview: id => Promise.resolve(this.previewOf(entries.get(id).source, primary))
137
+ });
138
+ return true;
139
+ }
140
+ refuse(key) {
141
+ const {
142
+ editor,
143
+ project
144
+ } = this.language;
145
+ const title = project.i18n.t(key);
146
+ project.host.notify?.({
147
+ id: TOAST_ID,
148
+ kind: 'error',
149
+ title
150
+ });
151
+ editor.announce(title);
152
+ }
153
+ /* The ranges to surround, their language and what they can be surrounded with; null where nothing can be. */
154
+ subjectOf() {
155
+ const {
156
+ editor,
157
+ languageId
158
+ } = this.language;
159
+ const candidates = editor.getSelections().flatMap(selection => {
160
+ if (comparePositions(selection.start, selection.end) !== 0) {
161
+ return [selection];
162
+ }
163
+ const line = lineTextOf(editor, selection.start.line);
164
+ const content = line.trim();
165
+ if (content === '') {
166
+ return [];
167
+ }
168
+ const start = line.length - line.trimStart().length;
169
+ return [{
170
+ start: {
171
+ line: selection.start.line,
172
+ character: start
173
+ },
174
+ end: {
175
+ line: selection.start.line,
176
+ character: start + content.length
177
+ }
178
+ }];
179
+ });
180
+ const ranges = candidates.length === 0 ? null : mergedRanges(candidates);
181
+ const found = ranges?.map(range => editor.surroundShape?.(range) ?? null) ?? [];
182
+ const shapes = found.filter(shape => shape !== null);
183
+ if (shapes.length === 0 || shapes.length !== found.length) {
184
+ return null;
185
+ }
186
+ const languages = shapes.flatMap(shape => [shape.range.start, shape.range.end]).map(position => editor.placeAt?.(position)?.language ?? languageId);
187
+ if (new Set(languages).size !== 1) {
188
+ return null;
189
+ }
190
+ return {
191
+ placements: shapes.map(shape => ({
192
+ range: shape.range,
193
+ selectedText: dedentLines(editor.textInRange(shape.range))
194
+ })),
195
+ language: languages[0],
196
+ context: templateContextAt(this.language, shapes.at(-1).range.start),
197
+ statements: shapes.every(shape => shape.statements),
198
+ expression: shapes.every(shape => shape.expression)
199
+ };
200
+ }
201
+ groupsOf(subject, templatesOnly) {
202
+ const {
203
+ project
204
+ } = this.language;
205
+ const {
206
+ families
207
+ } = subject.context;
208
+ const taken = new Set();
209
+ let next = 0;
210
+ const numbered = () => {
211
+ const mnemonic = MNEMONICS[next++];
212
+ if (mnemonic !== undefined) {
213
+ taken.add(mnemonic);
214
+ }
215
+ return mnemonic;
216
+ };
217
+ const insert = (id, label, detail, source, mnemonic) => ({
218
+ row: {
219
+ id,
220
+ label,
221
+ detail,
222
+ mnemonic
223
+ },
224
+ source,
225
+ run: () => this.insert(source, subject.placements)
226
+ });
227
+ const own = [];
228
+ if (!templatesOnly) {
229
+ const constructs = [...(subject.statements ? statementConstructs(families) : []), ...(subject.expression ? expressionConstructs(families) : []), ...regionConstructs(commentSyntax(subject.language))];
230
+ for (const construct of constructs) {
231
+ const region = construct.id === 'region' || construct.id === 'editorFold';
232
+ const label = region ? project.i18n.t('editor:language.surround.comments', {
233
+ construct: construct.label
234
+ }) : construct.label;
235
+ own.push(insert(`construct:${construct.id}`, label, '', construct.body, numbered()));
236
+ }
237
+ }
238
+ const live = [];
239
+ const markup = this.language.markup;
240
+ if (markup.supported && (families.includes('html') || families.includes('xml')) && this.language.editor.getSelections().length === 1) {
241
+ live.push({
242
+ row: {
243
+ id: 'emmet',
244
+ label: 'Emmet',
245
+ detail: '',
246
+ mnemonic: mnemonicOf('Emmet', taken)
247
+ },
248
+ source: null,
249
+ run: () => {
250
+ this.language.editor.setSelection(subject.placements[0].range);
251
+ markup.startWrap();
252
+ }
253
+ });
254
+ }
255
+ for (const template of project.templates.live) {
256
+ if (snippetVariables(template.body).has(SELECTED_TEXT) && templateApplies(template, subject.context)) {
257
+ live.push(insert(`template:${template.id}`, template.key, template.description ?? '', template.body, mnemonicOf(template.key, taken)));
258
+ }
259
+ }
260
+ const configure = project.host.openTemplateSettings?.bind(project.host);
261
+ if (configure !== undefined) {
262
+ live.push({
263
+ row: {
264
+ id: 'configure',
265
+ label: project.i18n.t('editor:language.surround.configure'),
266
+ detail: ''
267
+ },
268
+ source: null,
269
+ run: () => configure()
270
+ });
271
+ }
272
+ if (templatesOnly) {
273
+ return [{
274
+ title: null,
275
+ entries: live
276
+ }];
277
+ }
278
+ return [{
279
+ title: null,
280
+ entries: own
281
+ }, {
282
+ title: project.i18n.t('editor:language.surround.liveTemplates'),
283
+ entries: live
284
+ }].filter(group => group.entries.length > 0);
285
+ }
286
+ /* Inserts the snippet around every range; a snippet that still runs ends first, as the previous template does in IntelliJ. */
287
+ insert(source, placements) {
288
+ this.language.snippets.end();
289
+ void insertSnippet(this.language, bodyWithIndentUnit(source, this.indentUnit()), placements);
290
+ }
291
+ indentUnit() {
292
+ const {
293
+ tabSize,
294
+ insertSpaces
295
+ } = this.language.editor.getIndentation();
296
+ return insertSpaces ? ' '.repeat(tabSize) : '\t';
297
+ }
298
+ /* The lines the primary range covers, and the same lines with the snippet in the range's place. */
299
+ previewOf(source, placement) {
300
+ const {
301
+ editor
302
+ } = this.language;
303
+ if (source === null) {
304
+ return null;
305
+ }
306
+ const {
307
+ range
308
+ } = placement;
309
+ const before = textBeforeOf(editor, range.start);
310
+ const rendered = compileSnippet(bodyWithIndentUnit(source, this.indentUnit())).render(name => name === SELECTED_TEXT ? placement.selectedText ?? '' : undefined, undefined, {
311
+ indentVariables: true
312
+ });
313
+ const after = lineTextOf(editor, range.end.line).slice(range.end.character);
314
+ return {
315
+ removed: editor.textInRange({
316
+ start: {
317
+ line: range.start.line,
318
+ character: 0
319
+ },
320
+ end: {
321
+ line: range.end.line,
322
+ character: lineTextOf(editor, range.end.line).length
323
+ }
324
+ }).split(LINE_BREAK),
325
+ added: (before + indentSnippet(rendered, before).text + after).split(LINE_BREAK),
326
+ note: null
327
+ };
328
+ }
329
+ unwrapRows() {
330
+ const {
331
+ editor,
332
+ languageId,
333
+ project
334
+ } = this.language;
335
+ const unit = this.indentUnit();
336
+ const caret = editor.getCaret();
337
+ const text = editor.getText();
338
+ const syntax = markupSyntaxOf(languageId);
339
+ const scripts = (editor.unwrapTargets?.(caret) ?? []).map(target => {
340
+ const from = editor.offsetAt(target.range.start);
341
+ return {
342
+ label: project.i18n.t(`editor:language.surround.unwrap.${target.kind}`),
343
+ start: from,
344
+ from,
345
+ to: editor.offsetAt(target.range.end),
346
+ edits: target.edits.map(edit => ({
347
+ from: editor.offsetAt(edit.range.start),
348
+ to: editor.offsetAt(edit.range.end),
349
+ text: edit.text
350
+ })).sort((left, right) => left.from - right.from)
351
+ };
352
+ });
353
+ const tags = syntax === null ? [] : enclosingElements(text, editor.offsetAt(caret), syntax).map(element => ({
354
+ label: project.i18n.t('editor:language.surround.unwrap.tag', {
355
+ name: element.name
356
+ }),
357
+ start: element.open.from,
358
+ from: element.open.from,
359
+ to: element.close?.to ?? element.open.to,
360
+ edits: removeTagEdits(text, element, unit)
361
+ }));
362
+ // The sort is stable, so the rows of one construct stay in the order the editor gave them.
363
+ return [...scripts, ...tags].sort((left, right) => right.start - left.start);
364
+ }
365
+ applyUnwrap(row) {
366
+ const {
367
+ editor
368
+ } = this.language;
369
+ const changes = row.edits.map(edit => ({
370
+ range: {
371
+ start: editor.positionAt(edit.from),
372
+ end: editor.positionAt(edit.to)
373
+ },
374
+ text: edit.text
375
+ }));
376
+ const descending = [...changes].reverse();
377
+ editor.applyEdits(changes, {
378
+ mapSelections: range => shiftRange(range, descending)
379
+ });
380
+ editor.focus();
381
+ }
382
+ /* The lines of the construct before and after its edits. */
383
+ unwrapPreview(text, row) {
384
+ const edits = row.edits;
385
+ const from = lineStartOf(text, Math.min(row.from, ...edits.map(edit => edit.from)));
386
+ let last = Math.max(row.to, ...edits.map(edit => edit.to));
387
+ // An edit that takes a line break with it ends at the start of the next line, which is not part of the preview.
388
+ while (last > from && /[\r\n]/.test(text[last - 1])) {
389
+ last--;
390
+ }
391
+ const to = lineEndOf(text, last);
392
+ const after = [...edits].reverse().reduce((result, edit) => result.slice(0, edit.from - from) + edit.text + result.slice(edit.to - from), text.slice(from, to));
393
+ return {
394
+ removed: text.slice(from, to).split(LINE_BREAK),
395
+ added: after === '' ? [] : after.split(LINE_BREAK),
396
+ note: null
397
+ };
398
+ }
399
+ }
@@ -28,11 +28,10 @@ export class SymbolPickerFeature {
28
28
  }
29
29
  async open() {
30
30
  const {
31
- project,
32
31
  uri,
33
32
  popups
34
33
  } = this.language;
35
- if (!project.service.supports('textDocument/documentSymbol', uri)) {
34
+ if (!this.language.service.supports('textDocument/documentSymbol', uri)) {
36
35
  this.language.project.host.notify?.({
37
36
  id: TOAST_ID,
38
37
  kind: 'error',
@@ -41,7 +40,7 @@ export class SymbolPickerFeature {
41
40
  return;
42
41
  }
43
42
  try {
44
- const entries = entriesOf(await project.service.documentSymbols(uri));
43
+ const entries = entriesOf(await this.language.service.documentSymbols(uri));
45
44
  popups.setState({
46
45
  symbols: {
47
46
  file: basenameOf(this.language.uri),
@@ -87,13 +86,12 @@ export class SymbolPickerFeature {
87
86
  /* The symbols of the project that answer to a name, which is a request to the server and so waits for a pause in the typing. */
88
87
  async search(text, signal) {
89
88
  const {
90
- project,
91
89
  uri
92
90
  } = this.language;
93
- if (text === '' || !project.service.supports('workspace/symbol', uri)) {
91
+ if (text === '' || !this.language.service.supports('workspace/symbol', uri)) {
94
92
  return [];
95
93
  }
96
- return workspaceEntriesOf(await project.service.workspaceSymbols(uri, text, {
94
+ return workspaceEntriesOf(await this.language.service.workspaceSymbols(uri, text, {
97
95
  signal
98
96
  }));
99
97
  }
package/dist/symbols.js CHANGED
@@ -44,6 +44,23 @@ export function symbolSpansOf(result) {
44
44
  range: symbol.location.range
45
45
  }));
46
46
  }
47
+ function declarationsOf(result) {
48
+ if (result === null || result.length === 0) {
49
+ return [];
50
+ }
51
+ if (!('range' in result[0])) {
52
+ return result.map(symbol => symbol.location.range.start);
53
+ }
54
+ const stops = [];
55
+ const visit = symbols => {
56
+ for (const symbol of symbols) {
57
+ stops.push((symbol.selectionRange ?? symbol.range).start);
58
+ visit(symbol.children ?? []);
59
+ }
60
+ };
61
+ visit(result);
62
+ return stops;
63
+ }
47
64
  /* A line a declaration can have above it: blank, a comment, or an attribute. A bare `#` is no comment, since it heads a Markdown section. */
48
65
  const PRELUDE = /^\s*(?:$|\/\/|\/\*|\*|#\[)/;
49
66
  /*
@@ -112,18 +129,19 @@ export class SymbolsFeature {
112
129
  constructor(language, timers = realTimers) {
113
130
  const {
114
131
  editor,
115
- project,
116
132
  uri
117
133
  } = language;
118
134
  this.refresher = new Refresher(async signal => {
119
- if (!project.service.supports(METHOD, uri)) {
135
+ if (!language.service.supports(METHOD, uri)) {
120
136
  return;
121
137
  }
122
- const result = await project.service.documentSymbols(uri, {
138
+ const result = await language.service.documentSymbols(uri, {
123
139
  signal
124
140
  });
125
141
  if (!signal.aborted) {
126
142
  editor.setBlocks(result === null ? null : blocksOf(result, editor.getText()));
143
+ const declarations = declarationsOf(result);
144
+ editor.setDeclarations?.(declarations.length === 0 ? null : declarations);
127
145
  for (const listener of [...this.listeners]) {
128
146
  listener(result);
129
147
  }
@@ -138,6 +156,7 @@ export class SymbolsFeature {
138
156
  providers();
139
157
  this.refresher.dispose();
140
158
  editor.setBlocks(null);
159
+ editor.setDeclarations?.(null);
141
160
  });
142
161
  }
143
162
  /* Called with every answer that was not replaced before it came. */
@@ -0,0 +1,3 @@
1
+ import type { LiveTemplate, PostfixTemplate } from './templates.ts';
2
+ export declare const BUILT_IN_LIVE_TEMPLATES: readonly LiveTemplate[];
3
+ export declare const BUILT_IN_POSTFIX_TEMPLATES: readonly PostfixTemplate[];
@@ -0,0 +1,46 @@
1
+ /* A body of lines; a leading tab is one indentation unit. */
2
+ function body(...lines) {
3
+ return lines.join('\n');
4
+ }
5
+ function live(language, key, languages, text, description) {
6
+ return {
7
+ id: `builtin.${language}.${key}`,
8
+ key,
9
+ body: text,
10
+ description,
11
+ languages
12
+ };
13
+ }
14
+ function phpFunction(key, modifiers, description) {
15
+ return live('php', key, ['php'], body(`${modifiers} function \${1:name}($2)`, '{', '\t$0', '}'), description);
16
+ }
17
+ /* The keys and bodies of the live templates IntelliJ ships for JavaScript, PHP, HTML and XML. */
18
+ export const BUILT_IN_LIVE_TEMPLATES = [live('javascript', 'log', ['javascript'], 'console.log($0);', 'Prints a value to the console'), live('javascript', 'itar', ['javascript'], body('for (let ${1:i} = 0; $1 < ${2:array}.length; $1++) {', '\tconst ${3:element} = $2[$1];', '\t$0', '}'), 'Iterate elements of array'), live('javascript', 'ritar', ['javascript'], body('for (let ${1:i} = ${2:array}.length - 1; $1 >= 0; $1--) {', '\tconst ${3:element} = $2[$1];', '\t$0', '}'), 'Iterate elements of array in reverse order'), live('javascript', 'iter', ['javascript'], body('for (const ${1:item} of ${2:iterable}) {', '\t$0', '}'), 'Iterate (for...of)'), live('javascript', 'itin', ['javascript'], body('for (const ${1:key} in ${2:object}) {', '\t$0', '}'), 'Iterate (for...in)'), live('php', 'fore', ['php'], body('foreach (\\$${1:iterable} as \\$${2:value}) {', '\t$0', '}'), 'foreach(iterable_expr as $value) {...}'), live('php', 'forek', ['php'], body('foreach (\\$${1:iterable} as \\$${2:key} => \\$${3:value}) {', '\t$0', '}'), 'foreach(iterable_expr as $key => $value) {...}'), phpFunction('pubf', 'public', 'public function'), phpFunction('prif', 'private', 'private function'), phpFunction('prof', 'protected', 'protected function'), phpFunction('pubsf', 'public static', 'public static function'), live('php', 'thr', ['php'], 'throw new $0;', 'throw new'), live('html', 'T', ['html', 'xml'], '<${1:tag}>$TM_SELECTED_TEXT</${1/\\s.*//}>', 'Surround with <tag></tag>'), live('xml', 'CD', ['xml'], body('<![CDATA[', '$TM_SELECTED_TEXT', ']]>'), 'Surround with CDATA section')];
19
+ /* A block statement around the expression, with the caret inside it. */
20
+ function block(head) {
21
+ return body(`${head} {`, '\t$0', '}');
22
+ }
23
+ function script(key, text, example, description, expression = 'statement') {
24
+ return {
25
+ id: `builtin.postfix.javascript.${key}`,
26
+ key,
27
+ body: text,
28
+ example,
29
+ description,
30
+ languages: ['javascript', 'typescript'],
31
+ expression
32
+ };
33
+ }
34
+ function php(key, text, example, description, expression = 'statement') {
35
+ return {
36
+ id: `builtin.postfix.php.${key}`,
37
+ key,
38
+ body: text,
39
+ example,
40
+ description,
41
+ languages: ['php'],
42
+ expression
43
+ };
44
+ }
45
+ /* The postfix templates that ship with the editor, after the ones IntelliJ has for JavaScript, TypeScript and PHP. */
46
+ export const BUILT_IN_POSTFIX_TEMPLATES = [script('if', block('if ($EXPR)'), 'if (expr) {}', 'Checks boolean expression to be true'), script('else', block('if (!$EXPR)'), 'if (!expr) {}', 'Checks boolean expression to be false'), script('null', block('if ($EXPR === null)'), 'if (expr === null) {}', 'Checks expression to be null'), script('notnull', block('if ($EXPR !== null)'), 'if (expr !== null) {}', 'Checks expression to be not null'), script('return', 'return $EXPR;', 'return expr;', 'Returns the expression from the function'), script('throw', 'throw $EXPR;', 'throw expr;', 'Throws the expression'), script('const', 'const ${1:name} = $EXPR;', 'const name = expr;', 'Creates a constant from the expression'), script('let', 'let ${1:name} = $EXPR;', 'let name = expr;', 'Creates a variable from the expression'), script('forof', block('for (const ${1:item} of $EXPR)'), 'for (const item of expr) {}', 'Iterates over the elements (for...of)'), script('forin', block('for (const ${1:key} in $EXPR)'), 'for (const key in expr) {}', 'Iterates over the keys (for...in)'), script('log', 'console.log($EXPR);', 'console.log(expr);', 'Prints the expression to the console'), script('not', '!$EXPR', '!expr', 'Negates the expression', 'any'), script('par', '($EXPR)', '(expr)', 'Surrounds the expression with parentheses', 'any'), script('arg', '${1:fn}($EXPR)', 'fn(expr)', 'Passes the expression to a function call', 'any'), script('await', 'await $EXPR', 'await expr', 'Awaits the expression', 'any'), script('typeof', 'typeof $EXPR', 'typeof expr', 'Takes the type of the expression', 'any'), php('if', block('if ($EXPR)'), 'if (expr) {}', 'Checks boolean expression to be true'), php('isset', block('if (isset($EXPR))'), 'if (isset(expr)) {}', 'Checks that the expression is set'), php('empty', block('if (empty($EXPR))'), 'if (empty(expr)) {}', 'Checks that the expression is empty'), php('null', block('if ($EXPR === null)'), 'if (expr === null) {}', 'Checks expression to be null'), php('notnull', block('if ($EXPR !== null)'), 'if (expr !== null) {}', 'Checks expression to be not null'), php('return', 'return $EXPR;', 'return expr;', 'Returns the expression from the function'), php('throw', 'throw $EXPR;', 'throw expr;', 'Throws the expression'), php('var', '\\$${1:name} = $EXPR;', '$name = expr;', 'Creates a variable from the expression'), php('foreach', block('foreach ($EXPR as \\$${1:value})'), 'foreach (expr as $value) {}', 'Iterates over the elements'), php('not', '!$EXPR', '!expr', 'Negates the expression', 'any'), php('par', '($EXPR)', '(expr)', 'Surrounds the expression with parentheses', 'any')];
@@ -0,0 +1,12 @@
1
+ import type { EditorPosition } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import type { LiveTemplate, TemplateLanguage, TemplatePlace } from './templates.ts';
4
+ export declare const SNIPPET_KIND = 15;
5
+ export declare function sortAfterServer(key: string): string;
6
+ export declare function familiesOf(languageId: string): readonly TemplateLanguage[];
7
+ export interface TemplateContext {
8
+ readonly families: readonly TemplateLanguage[];
9
+ readonly place: TemplatePlace;
10
+ }
11
+ export declare function templateContextAt(language: EditorLanguage, position: EditorPosition): TemplateContext;
12
+ export declare function templateApplies(template: LiveTemplate, context: TemplateContext): boolean;
@@ -0,0 +1,56 @@
1
+ /* The completion item kind of a snippet. */
2
+ export const SNIPPET_KIND = 15;
3
+ /* Sorts a template row after the rows of the same score that a service answers. */
4
+ export function sortAfterServer(key) {
5
+ return `￿${key}`;
6
+ }
7
+ /* The families a language id counts as; TypeScript builds on JavaScript. */
8
+ export function familiesOf(languageId) {
9
+ switch (languageId) {
10
+ case 'javascript':
11
+ case 'js':
12
+ case 'mjs':
13
+ case 'cjs':
14
+ case 'jsx':
15
+ case 'javascriptreact':
16
+ return ['javascript'];
17
+ case 'typescript':
18
+ case 'ts':
19
+ case 'mts':
20
+ case 'cts':
21
+ case 'tsx':
22
+ case 'typescriptreact':
23
+ return ['javascript', 'typescript'];
24
+ case 'php':
25
+ return ['php'];
26
+ case 'css':
27
+ case 'scss':
28
+ case 'less':
29
+ return ['css'];
30
+ case 'html':
31
+ case 'xhtml':
32
+ case 'vue':
33
+ return ['html'];
34
+ case 'xml':
35
+ case 'svg':
36
+ case 'xsl':
37
+ return ['xml'];
38
+ default:
39
+ return ['other'];
40
+ }
41
+ }
42
+ /* The families and the place at a position; an editor that cannot tell has the language of the document, in code. */
43
+ export function templateContextAt(language, position) {
44
+ const place = language.editor.placeAt?.(position);
45
+ return place === undefined ? {
46
+ families: familiesOf(language.languageId),
47
+ place: 'code'
48
+ } : {
49
+ families: familiesOf(place.language),
50
+ place: place.kind
51
+ };
52
+ }
53
+ /* Whether a template shares a family with the context and applies to its place. */
54
+ export function templateApplies(template, context) {
55
+ return template.languages.some(family => context.families.includes(family)) && (template.places ?? ['code']).includes(context.place);
56
+ }
@@ -0,0 +1,59 @@
1
+ import { BUILT_IN_LIVE_TEMPLATES, BUILT_IN_POSTFIX_TEMPLATES } from './template-builtins.ts';
2
+ export { BUILT_IN_LIVE_TEMPLATES, BUILT_IN_POSTFIX_TEMPLATES };
3
+ export { type TemplatesFeature } from './live-templates.ts';
4
+ export type TemplateLanguage = 'javascript' | 'typescript' | 'php' | 'css' | 'html' | 'xml' | 'other';
5
+ export type TemplatePlace = 'code' | 'comment' | 'string';
6
+ export interface LiveTemplate {
7
+ readonly id: string;
8
+ readonly key: string;
9
+ readonly body: string;
10
+ readonly description?: string;
11
+ readonly languages: readonly TemplateLanguage[];
12
+ readonly places?: readonly TemplatePlace[];
13
+ readonly group?: string;
14
+ }
15
+ export interface PostfixTemplate {
16
+ readonly id: string;
17
+ readonly key: string;
18
+ readonly body: string;
19
+ readonly description?: string;
20
+ readonly example?: string;
21
+ readonly languages: readonly ('javascript' | 'typescript' | 'php')[];
22
+ readonly expression?: 'any' | 'statement';
23
+ }
24
+ export interface TemplateSettings {
25
+ readonly liveInCompletion?: boolean;
26
+ readonly postfix?: boolean;
27
+ readonly postfixInCompletion?: boolean;
28
+ }
29
+ export interface LiveTemplateDraft {
30
+ readonly body: string;
31
+ readonly languages: readonly TemplateLanguage[];
32
+ readonly places: readonly TemplatePlace[];
33
+ }
34
+ export interface TemplateRegistryOptions {
35
+ readonly builtins?: boolean;
36
+ }
37
+ export interface TemplateUpdate {
38
+ readonly live?: readonly LiveTemplate[];
39
+ readonly postfix?: readonly PostfixTemplate[];
40
+ readonly disabled?: readonly string[];
41
+ readonly settings?: TemplateSettings;
42
+ }
43
+ export declare class TemplateRegistry {
44
+ private readonly builtins;
45
+ private readonly listeners;
46
+ private own;
47
+ private off;
48
+ private current;
49
+ private liveList;
50
+ private postfixList;
51
+ constructor(options?: TemplateRegistryOptions);
52
+ get live(): readonly LiveTemplate[];
53
+ get postfix(): readonly PostfixTemplate[];
54
+ get settings(): Required<TemplateSettings>;
55
+ set(update: TemplateUpdate): void;
56
+ onChange(listener: () => void): () => void;
57
+ private listed;
58
+ }
59
+ export declare function checkTemplate(template: LiveTemplate | PostfixTemplate, kind?: 'live' | 'postfix'): string | null;