@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
package/dist/markup.js ADDED
@@ -0,0 +1,465 @@
1
+ import { abbreviationBefore, expandAbbreviation } from './emmet.js';
2
+ import { KEY_PRIORITY } from './key-priority.js';
3
+ import { lineTextOf } from './line-text.js';
4
+ import { isVoidElement, markupPlaceAt, markupSyntaxOf, tagPairAt } from './markup-context.js';
5
+ import { isShortcut } from './shortcut-keys.js';
6
+ import { parseSnippet } from './snippet.js';
7
+ import { indentSnippet } from './snippet-indent.js';
8
+ import { positionInInsertion } from './snippet-session.js';
9
+ const NAME = /^[\w:.-]*$/;
10
+ const NAME_BEFORE = /<\/?[\w:.-]*$/;
11
+ /*
12
+ * Writing HTML, XML, the template of a Vue file and the markup of PHP: Emmet abbreviations, end tags that
13
+ * write themselves and tag names that change in pairs. The edits it adds join the undo step of the key that
14
+ * caused them. It does nothing in other languages, in a document over `MAX_MARKUP_SCAN` characters, or with more
15
+ * than one caret.
16
+ */
17
+ export class MarkupFeature {
18
+ language;
19
+ syntax;
20
+ settings = {
21
+ emmet: true,
22
+ closeTags: true,
23
+ linkedTags: true
24
+ };
25
+ link = null;
26
+ wrapping = null;
27
+ /* Raised by every change of the text, so a follow-up edit for an older text is dropped. */
28
+ revision = 0;
29
+ mirroring = false;
30
+ constructor(language) {
31
+ this.language = language;
32
+ this.syntax = markupSyntaxOf(language.languageId);
33
+ if (this.syntax === null) {
34
+ return;
35
+ }
36
+ const {
37
+ editor
38
+ } = language;
39
+ const offs = [language.onKey(event => this.key(event), KEY_PRIORITY.markup), editor.onTextChange(change => this.edited(change)), editor.onCaret(() => this.moved())];
40
+ language.onDispose(() => {
41
+ this.cancelWrap();
42
+ for (const off of offs) {
43
+ off();
44
+ }
45
+ });
46
+ }
47
+ /* Whether the language of the document is markup this feature writes. */
48
+ get supported() {
49
+ return this.syntax !== null;
50
+ }
51
+ configure(settings) {
52
+ this.settings = {
53
+ emmet: settings.emmet !== false,
54
+ closeTags: settings.closeTags !== false,
55
+ linkedTags: settings.linkedTags !== false
56
+ };
57
+ if (!this.settings.linkedTags) {
58
+ this.link = null;
59
+ }
60
+ }
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
+ 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() {
71
+ const {
72
+ syntax
73
+ } = this;
74
+ const {
75
+ editor
76
+ } = this.language;
77
+ if (syntax === null || !this.settings.emmet || !this.editable()) {
78
+ return null;
79
+ }
80
+ const caret = editor.getCaret();
81
+ const line = lineTextOf(editor, caret.line);
82
+ const found = abbreviationBefore(line, caret.character);
83
+ if (found === null) {
84
+ return null;
85
+ }
86
+ const range = {
87
+ start: {
88
+ line: caret.line,
89
+ character: found.start
90
+ },
91
+ end: {
92
+ line: caret.line,
93
+ character: found.end
94
+ }
95
+ };
96
+ if (markupPlaceAt(editor.getText(), editor.offsetAt(range.start), syntax)?.kind !== 'text') {
97
+ return null;
98
+ }
99
+ const expanded = expandAbbreviation(found.abbreviation, {
100
+ syntax,
101
+ indent: this.indent()
102
+ });
103
+ return expanded === null ? null : {
104
+ range,
105
+ expanded,
106
+ line
107
+ };
108
+ }
109
+ /* Opens the field for an abbreviation to wrap the selection in, or the text of the caret's line without one. */
110
+ startWrap() {
111
+ const {
112
+ editor,
113
+ popups
114
+ } = this.language;
115
+ if (this.syntax === null || !this.editable(true)) {
116
+ return false;
117
+ }
118
+ const range = this.wrapTarget();
119
+ if (range === null) {
120
+ return false;
121
+ }
122
+ this.cancelWrap();
123
+ this.wrapping = editor.trackRange(range);
124
+ popups.setState({
125
+ wrap: {
126
+ anchor: range.start,
127
+ error: null
128
+ }
129
+ });
130
+ return true;
131
+ }
132
+ /* Wraps the text the field was opened over; false, with the reason in the field, when the abbreviation does not expand. */
133
+ submitWrap(abbreviation) {
134
+ const {
135
+ syntax
136
+ } = this;
137
+ const {
138
+ editor,
139
+ popups,
140
+ project
141
+ } = this.language;
142
+ const range = this.wrapping?.get() ?? null;
143
+ if (syntax === null || range === null) {
144
+ this.cancelWrap();
145
+ return false;
146
+ }
147
+ const expanded = expandAbbreviation(abbreviation.trim(), {
148
+ syntax,
149
+ indent: this.indent(),
150
+ wrap: this.wrapLines(range)
151
+ });
152
+ if (expanded === null) {
153
+ popups.setState({
154
+ wrap: {
155
+ anchor: range.start,
156
+ error: project.i18n.t('editor:language.markup.invalid')
157
+ }
158
+ });
159
+ return false;
160
+ }
161
+ this.cancelWrap();
162
+ const inserted = this.insert(range, expanded, lineTextOf(editor, range.start.line));
163
+ editor.focus();
164
+ return inserted;
165
+ }
166
+ cancelWrap() {
167
+ this.wrapping?.dispose();
168
+ this.wrapping = null;
169
+ if (this.language.popups.getState().wrap !== null) {
170
+ this.language.popups.setState({
171
+ wrap: null
172
+ });
173
+ }
174
+ }
175
+ key(event) {
176
+ if (isShortcut(this.language.shortcuts.wrapWithAbbreviation, event)) {
177
+ return this.startWrap();
178
+ }
179
+ if (event.key !== 'Tab' || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || event.isComposing) {
180
+ return false;
181
+ }
182
+ return this.expand();
183
+ }
184
+ editable(selection = false) {
185
+ const {
186
+ editor
187
+ } = this.language;
188
+ const selections = editor.getSelections();
189
+ if (editor.isReadOnly?.() === true || selections.length !== 1) {
190
+ return false;
191
+ }
192
+ const {
193
+ start,
194
+ end
195
+ } = selections[0];
196
+ return selection || start.line === end.line && start.character === end.character;
197
+ }
198
+ indent() {
199
+ const {
200
+ tabSize,
201
+ insertSpaces
202
+ } = this.language.editor.getIndentation();
203
+ return insertSpaces ? ' '.repeat(tabSize) : '\t';
204
+ }
205
+ /* The selection without the whitespace around it, or the text of the caret's line; null when that is blank. */
206
+ wrapTarget() {
207
+ const {
208
+ editor
209
+ } = this.language;
210
+ const selection = editor.getSelection();
211
+ const text = editor.getText();
212
+ const empty = selection.start.line === selection.end.line && selection.start.character === selection.end.character;
213
+ let from = empty ? editor.offsetAt({
214
+ line: selection.start.line,
215
+ character: 0
216
+ }) : editor.offsetAt(selection.start);
217
+ let to = empty ? from + lineTextOf(editor, selection.start.line).length : editor.offsetAt(selection.end);
218
+ while (from < to && /\s/.test(text[from])) {
219
+ from++;
220
+ }
221
+ while (to > from && /\s/.test(text[to - 1])) {
222
+ to--;
223
+ }
224
+ return from === to ? null : {
225
+ start: editor.positionAt(from),
226
+ end: editor.positionAt(to)
227
+ };
228
+ }
229
+ /* The lines of the wrapped text, without the indentation they share, which the expansion gives them again. */
230
+ wrapLines(range) {
231
+ const lines = this.language.editor.textInRange(range).split(/\r\n|\r|\n/);
232
+ const indents = lines.slice(1).flatMap(line => line.trim() === '' ? [] : [/^[\t ]*/.exec(line)[0].length]);
233
+ const shared = indents.length === 0 ? 0 : Math.min(...indents);
234
+ return lines.map((line, index) => index === 0 ? line : line.slice(Math.min(shared, /^[\t ]*/.exec(line)[0].length)));
235
+ }
236
+ insert(range, snippet, line) {
237
+ const {
238
+ editor,
239
+ snippets
240
+ } = this.language;
241
+ const parsed = indentSnippet(parseSnippet(snippet), line);
242
+ if (parsed.limited === true) {
243
+ return false;
244
+ }
245
+ if (!this.apply([{
246
+ range,
247
+ text: parsed.text
248
+ }])) {
249
+ return false;
250
+ }
251
+ if (parsed.stops.length > 0) {
252
+ snippets.begin(range.start, parsed.text, parsed.stops, parsed.fields);
253
+ } else {
254
+ editor.setCaret(positionInInsertion(range.start, parsed.text, parsed.text.length));
255
+ }
256
+ return true;
257
+ }
258
+ /* Through the snippet session while one runs, so an edit inside its field keeps it going. */
259
+ apply(changes, options) {
260
+ const {
261
+ editor,
262
+ snippets
263
+ } = this.language;
264
+ return snippets.isActive ? snippets.applyEdits(changes, snippets.token, [], options) : editor.applyEdits(changes, options);
265
+ }
266
+ edited(change) {
267
+ const revision = ++this.revision;
268
+ if (this.mirroring) {
269
+ return;
270
+ }
271
+ const edit = change.changes[0];
272
+ // Backspace and the word deletes are commands too; an undo of both names is two changes and lets go.
273
+ if (change.source === 'external' || change.changes.length !== 1 || edit === undefined || edit.range.start.line !== edit.range.end.line) {
274
+ this.link = null;
275
+ return;
276
+ }
277
+ const from = this.language.editor.offsetAt(edit.range.start);
278
+ const removed = edit.range.end.character - edit.range.start.character;
279
+ this.follow(from, removed, edit.text, revision);
280
+ if (change.source !== 'input' || !this.settings.closeTags || removed !== 0 || this.language.editor.getSelections().length !== 1) {
281
+ return;
282
+ }
283
+ if (edit.text === '>') {
284
+ queueMicrotask(() => this.closeTag(from, revision));
285
+ } else if (edit.text === '/') {
286
+ queueMicrotask(() => this.completeEndTag(from, revision));
287
+ }
288
+ }
289
+ /* Keeps the link on the name being typed and copies the name to its other tag once the edit is in. */
290
+ follow(from, removed, inserted, revision) {
291
+ const link = this.link;
292
+ if (link === null) {
293
+ return;
294
+ }
295
+ const {
296
+ own,
297
+ partner
298
+ } = link;
299
+ if (from < own.from || from + removed > own.to || !NAME.test(inserted)) {
300
+ this.link = null;
301
+ return;
302
+ }
303
+ const delta = inserted.length - removed;
304
+ this.link = {
305
+ own: {
306
+ from: own.from,
307
+ to: own.to + delta
308
+ },
309
+ partner: partner.from > own.from ? {
310
+ from: partner.from + delta,
311
+ to: partner.to + delta
312
+ } : partner
313
+ };
314
+ queueMicrotask(() => this.mirror(revision));
315
+ }
316
+ mirror(revision) {
317
+ const link = this.link;
318
+ if (link === null || revision !== this.revision) {
319
+ return;
320
+ }
321
+ const {
322
+ editor
323
+ } = this.language;
324
+ const text = editor.getText();
325
+ const name = text.slice(link.own.from, link.own.to);
326
+ if (name === text.slice(link.partner.from, link.partner.to)) {
327
+ return;
328
+ }
329
+ const range = {
330
+ start: editor.positionAt(link.partner.from),
331
+ end: editor.positionAt(link.partner.to)
332
+ };
333
+ this.mirroring = true;
334
+ try {
335
+ if (!this.apply([{
336
+ range,
337
+ text: name
338
+ }], {
339
+ mergeUndo: true
340
+ })) {
341
+ this.link = null;
342
+ return;
343
+ }
344
+ } finally {
345
+ this.mirroring = false;
346
+ }
347
+ const delta = name.length - (link.partner.to - link.partner.from);
348
+ this.link = {
349
+ own: link.partner.from < link.own.from ? {
350
+ from: link.own.from + delta,
351
+ to: link.own.to + delta
352
+ } : link.own,
353
+ partner: {
354
+ from: link.partner.from,
355
+ to: link.partner.to + delta
356
+ }
357
+ };
358
+ }
359
+ /* The `>` at `at` ended a start tag: its end tag goes after it, with the caret between the two. */
360
+ closeTag(at, revision) {
361
+ const {
362
+ editor
363
+ } = this.language;
364
+ const text = editor.getText();
365
+ if (revision !== this.revision || text[at] !== '>' || text[at - 1] === '/') {
366
+ return;
367
+ }
368
+ const place = markupPlaceAt(text, at, this.syntax);
369
+ if (place?.kind !== 'tag' || place.tag.closing || place.tag.inValue || place.tag.name === '' || isVoidElement(place.tag.name, this.syntax)) {
370
+ return;
371
+ }
372
+ const end = `</${place.tag.name}>`;
373
+ if (text.startsWith(end, at + 1)) {
374
+ return;
375
+ }
376
+ const caret = editor.positionAt(at + 1);
377
+ this.apply([{
378
+ range: {
379
+ start: caret,
380
+ end: caret
381
+ },
382
+ text: end
383
+ }], {
384
+ mergeUndo: true,
385
+ mapSelections: () => ({
386
+ start: caret,
387
+ end: caret
388
+ })
389
+ });
390
+ }
391
+ /* The `/` at `at` follows a `<` in text: the end tag of the innermost open element is written out. */
392
+ completeEndTag(at, revision) {
393
+ const {
394
+ editor
395
+ } = this.language;
396
+ const text = editor.getText();
397
+ if (revision !== this.revision || text[at] !== '/' || text[at - 1] !== '<') {
398
+ return;
399
+ }
400
+ const place = markupPlaceAt(text, at - 1, this.syntax);
401
+ const element = place?.kind === 'text' ? place.open.at(-1) : undefined;
402
+ if (element === undefined) {
403
+ return;
404
+ }
405
+ const position = editor.positionAt(at + 1);
406
+ const closed = text[at + 1] === '>';
407
+ const completion = closed ? element.name : `${element.name}>`;
408
+ const after = {
409
+ line: position.line,
410
+ character: position.character + element.name.length + 1
411
+ };
412
+ this.apply([{
413
+ range: {
414
+ start: position,
415
+ end: position
416
+ },
417
+ text: completion
418
+ }], {
419
+ mergeUndo: true,
420
+ mapSelections: () => ({
421
+ start: after,
422
+ end: after
423
+ })
424
+ });
425
+ }
426
+ /* Links the tag name the caret moved into with its other tag, once the edit that moved it is in. */
427
+ moved() {
428
+ if (!this.settings.linkedTags || this.mirroring) {
429
+ return;
430
+ }
431
+ queueMicrotask(() => {
432
+ this.link = this.linkAtCaret();
433
+ });
434
+ }
435
+ linkAtCaret() {
436
+ const {
437
+ editor
438
+ } = this.language;
439
+ if (!this.editable()) {
440
+ return null;
441
+ }
442
+ const caret = editor.getCaret();
443
+ const offset = editor.offsetAt(caret);
444
+ const link = this.link;
445
+ if (link !== null && link.own.from <= offset && offset <= link.own.to) {
446
+ return link;
447
+ }
448
+ const line = lineTextOf(editor, caret.line);
449
+ if (!NAME_BEFORE.test(line.slice(0, caret.character))) {
450
+ return null;
451
+ }
452
+ const pair = tagPairAt(editor.getText(), offset, this.syntax);
453
+ if (pair === null) {
454
+ return null;
455
+ }
456
+ const inOpen = pair.open.from <= offset && offset <= pair.open.to;
457
+ return inOpen ? {
458
+ own: pair.open,
459
+ partner: pair.close
460
+ } : {
461
+ own: pair.close,
462
+ partner: pair.open
463
+ };
464
+ }
465
+ }
@@ -75,11 +75,10 @@ export class NavigationFeature {
75
75
  });
76
76
  }
77
77
  supports(kind) {
78
- return this.language.project.service.supports(METHODS[kind], this.language.uri);
78
+ return this.language.service.supports(METHODS[kind], this.language.uri);
79
79
  }
80
80
  async go(kind, position = this.language.editor.getCaret()) {
81
81
  const {
82
- project,
83
82
  uri,
84
83
  editor
85
84
  } = this.language;
@@ -89,7 +88,7 @@ export class NavigationFeature {
89
88
  }
90
89
  let result;
91
90
  try {
92
- result = await REQUESTS[kind](project.service, uri, position);
91
+ result = await REQUESTS[kind](this.language.service, uri, position);
93
92
  } catch (error) {
94
93
  if (!(error instanceof StaleResultError)) {
95
94
  this.tell(this.say('failed', {
@@ -118,7 +117,6 @@ export class NavigationFeature {
118
117
  */
119
118
  async goToName(name, anchor, own) {
120
119
  const {
121
- project,
122
120
  uri,
123
121
  editor
124
122
  } = this.language;
@@ -126,11 +124,11 @@ export class NavigationFeature {
126
124
  this.language.goTo(own);
127
125
  return;
128
126
  }
129
- if (!project.service.supports('workspace/symbol', uri)) {
127
+ if (!this.language.service.supports('workspace/symbol', uri)) {
130
128
  return;
131
129
  }
132
130
  try {
133
- const places = placesOfName(await project.service.workspaceSymbols(uri, name), name, editor.getText());
131
+ const places = placesOfName(await this.language.service.workspaceSymbols(uri, name), name, editor.getText());
134
132
  if (places.length === 1) {
135
133
  this.language.goTo(places[0]);
136
134
  } else if (places.length > 1) {
@@ -0,0 +1,7 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ export declare class PasteHistoryFeature {
3
+ private readonly language;
4
+ constructor(language: EditorLanguage);
5
+ open(): void;
6
+ private paste;
7
+ }
@@ -0,0 +1,104 @@
1
+ import { rememberCopy } from '@adecore/editor';
2
+ import { KEY_PRIORITY } from './key-priority.js';
3
+ import { isShortcut } from './shortcut-keys.js';
4
+ const LABEL_LENGTH = 80;
5
+ /* The first characters of a copy on one line, with each line break shown as a return sign. */
6
+ function labelOf(text) {
7
+ const flat = text.replace(/\r\n|\r|\n/g, '⏎');
8
+ return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH)}…` : flat;
9
+ }
10
+ /*
11
+ * Paste from History: a list of what was copied in the editors of the project, newest first. Typing narrows it,
12
+ * Shift with the arrows chooses several entries, which paste as one, and Delete forgets entries.
13
+ */
14
+ export class PasteHistoryFeature {
15
+ language;
16
+ constructor(language) {
17
+ this.language = language;
18
+ const off = language.onKey(event => {
19
+ if (!isShortcut(this.language.shortcuts.pasteFromHistory, event)) {
20
+ return false;
21
+ }
22
+ this.open();
23
+ return true;
24
+ }, KEY_PRIORITY.pasteHistory);
25
+ language.onDispose(off);
26
+ }
27
+ open() {
28
+ const {
29
+ editor,
30
+ project
31
+ } = this.language;
32
+ const {
33
+ clipboard
34
+ } = project;
35
+ if (clipboard.entries.length === 0) {
36
+ editor.announce(project.i18n.t('editor:language.pasteHistory.empty'));
37
+ return;
38
+ }
39
+ const ids = new Map();
40
+ const entries = new Map();
41
+ const groups = () => [{
42
+ title: null,
43
+ rows: clipboard.entries.map(entry => {
44
+ const id = ids.get(entry) ?? String(ids.size);
45
+ ids.set(entry, id);
46
+ entries.set(id, entry);
47
+ return {
48
+ id,
49
+ label: labelOf(entry.text),
50
+ detail: '',
51
+ search: entry.text
52
+ };
53
+ })
54
+ }];
55
+ const rows = groups();
56
+ this.language.pick.open({
57
+ anchor: editor.getCaret(),
58
+ title: project.i18n.t('editor:language.pasteHistory.title'),
59
+ groups: rows,
60
+ active: rows[0].rows[0].id,
61
+ filter: true,
62
+ multiple: true,
63
+ accept: id => this.paste([entries.get(id)]),
64
+ acceptMany: chosen => this.paste(clipboard.entries.filter(entry => chosen.includes(ids.get(entry) ?? ''))),
65
+ preview: async id => ({
66
+ removed: [],
67
+ added: entries.get(id)?.text.split('\n') ?? [],
68
+ note: null,
69
+ kind: 'text'
70
+ }),
71
+ remove: chosen => {
72
+ for (const id of chosen) {
73
+ clipboard.remove(entries.get(id));
74
+ }
75
+ return groups();
76
+ }
77
+ });
78
+ }
79
+ /* Pastes entries in list order, joined by line breaks, as a paste of that text from the clipboard would. */
80
+ paste(chosen) {
81
+ const {
82
+ editor,
83
+ project
84
+ } = this.language;
85
+ const {
86
+ clipboard
87
+ } = project;
88
+ if (chosen.length === 0) {
89
+ return;
90
+ }
91
+ const text = chosen.map(entry => entry.text).join('\n');
92
+ const wholeLines = chosen.length === 1 && chosen[0].wholeLines;
93
+ if (chosen.length === 1) {
94
+ clipboard.promote(chosen[0]);
95
+ } else {
96
+ clipboard.add(text);
97
+ }
98
+ globalThis.navigator?.clipboard?.writeText(text).catch(() => undefined);
99
+ rememberCopy(text, wholeLines);
100
+ editor.paste?.(text, {
101
+ wholeLines
102
+ });
103
+ }
104
+ }
package/dist/peek.js CHANGED
@@ -71,7 +71,7 @@ export class PeekFeature {
71
71
  uri
72
72
  } = this.language;
73
73
  const method = KINDS[kind].method;
74
- if (!project.service.supports(method, uri)) {
74
+ if (!this.language.service.supports(method, uri)) {
75
75
  this.tell(this.say(KINDS[kind].unavailable));
76
76
  return;
77
77
  }
@@ -147,17 +147,16 @@ export class PeekFeature {
147
147
  }
148
148
  async fetch(kind, position) {
149
149
  const {
150
- project,
151
150
  uri
152
151
  } = this.language;
153
152
  if (kind === 'references') {
154
153
  // Without the declaration, so the list holds what the usages count above a declaration counts.
155
154
  return {
156
- locations: (await project.service.references(uri, position, false)) ?? [],
155
+ locations: (await this.language.service.references(uri, position, false)) ?? [],
157
156
  targets: new Map()
158
157
  };
159
158
  }
160
- const places = uniquePlaces(await project.service.definition(uri, position));
159
+ const places = uniquePlaces(await this.language.service.definition(uri, position));
161
160
  const targets = new Map(places.flatMap(({
162
161
  location,
163
162
  target
package/dist/pick.d.ts CHANGED
@@ -11,13 +11,21 @@ export interface PickSpec {
11
11
  onTab?(backwards: boolean): void;
12
12
  attempt?(id: string): Promise<string | null>;
13
13
  preview?(id: string, signal: AbortSignal): Promise<PickPreview | null>;
14
+ readonly filter?: boolean;
15
+ readonly multiple?: boolean;
16
+ acceptMany?(ids: readonly string[]): void;
17
+ remove?(ids: readonly string[]): readonly PickGroup[];
14
18
  }
15
19
  export declare class PickFeature {
16
20
  private readonly language;
17
21
  private readonly timers;
18
22
  private spec;
23
+ private groups;
19
24
  private ids;
20
25
  private active;
26
+ private query;
27
+ private chosen;
28
+ private rangeAnchor;
21
29
  private previewTimer;
22
30
  private controller;
23
31
  private refusals;
@@ -26,10 +34,21 @@ export declare class PickFeature {
26
34
  get isOpen(): boolean;
27
35
  open(spec: PickSpec): void;
28
36
  close(): void;
29
- move(delta: number): void;
30
- choose(id: string): void;
37
+ move(delta: number, extend?: boolean): void;
38
+ private settle;
39
+ choose(id: string, modifiers?: {
40
+ readonly toggle?: boolean;
41
+ readonly range?: boolean;
42
+ }): void;
31
43
  private activate;
44
+ private extendTo;
45
+ private toggle;
46
+ private takeActive;
47
+ private visibleGroups;
48
+ private requery;
49
+ private removeChosen;
32
50
  private key;
51
+ private takeMnemonic;
33
52
  private schedulePreview;
34
53
  private publish;
35
54
  }