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