@adecore/editor-react 0.41.1 → 0.43.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 (124) hide show
  1. package/dist/CallHierarchyPanel.js +177 -181
  2. package/dist/EditorContextMenu.js +118 -105
  3. package/dist/EditorView.js +7 -1
  4. package/dist/LanguagePopups.js +7 -0
  5. package/dist/PickPopup.js +190 -70
  6. package/dist/WrapCard.d.ts +8 -0
  7. package/dist/WrapCard.js +172 -0
  8. package/dist/call-hierarchy-model.d.ts +4 -2
  9. package/dist/call-hierarchy-model.js +20 -0
  10. package/dist/call-hierarchy.d.ts +3 -0
  11. package/dist/call-hierarchy.js +54 -22
  12. package/dist/clipboard-history.d.ts +31 -0
  13. package/dist/clipboard-history.js +70 -0
  14. package/dist/code-actions.d.ts +3 -1
  15. package/dist/code-actions.js +50 -10
  16. package/dist/code-vision.js +2 -3
  17. package/dist/completion.js +10 -15
  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 +19 -1
  30. package/dist/editor-language.js +77 -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 +6 -0
  39. package/dist/index.d.ts +11 -2
  40. package/dist/index.js +8 -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 +8 -5
  45. package/dist/key-priority.js +9 -6
  46. package/dist/linked-editing.d.ts +26 -0
  47. package/dist/linked-editing.js +215 -0
  48. package/dist/locales/en.json +31 -2
  49. package/dist/locales/nl.json +31 -2
  50. package/dist/markup-context.d.ts +37 -0
  51. package/dist/markup-context.js +291 -0
  52. package/dist/markup.d.ts +36 -0
  53. package/dist/markup.js +453 -0
  54. package/dist/navigation.js +4 -6
  55. package/dist/paste-history.d.ts +7 -0
  56. package/dist/paste-history.js +104 -0
  57. package/dist/peek.js +3 -4
  58. package/dist/pick.d.ts +20 -2
  59. package/dist/pick.js +141 -18
  60. package/dist/popups.d.ts +9 -0
  61. package/dist/popups.js +2 -1
  62. package/dist/project-language.d.ts +18 -1
  63. package/dist/project-language.js +129 -18
  64. package/dist/rename.js +8 -11
  65. package/dist/rendered-docs.js +1 -2
  66. package/dist/selection-ranges.js +2 -3
  67. package/dist/shortcut-keys.d.ts +27 -0
  68. package/dist/signature.js +3 -6
  69. package/dist/snippet-session.d.ts +2 -2
  70. package/dist/snippet-session.js +3 -3
  71. package/dist/symbol-picker.js +4 -6
  72. package/dist/symbols.js +22 -3
  73. package/dist/testing.d.ts +23 -1
  74. package/dist/testing.js +114 -0
  75. package/package.json +7 -6
  76. package/src/CallHierarchyPanel.tsx +26 -11
  77. package/src/EditorContextMenu.tsx +7 -0
  78. package/src/EditorView.tsx +9 -0
  79. package/src/LanguagePopups.tsx +4 -0
  80. package/src/PickPopup.tsx +40 -5
  81. package/src/WrapCard.tsx +64 -0
  82. package/src/call-hierarchy-model.ts +25 -3
  83. package/src/call-hierarchy.ts +62 -20
  84. package/src/clipboard-history.ts +102 -0
  85. package/src/code-actions.ts +48 -11
  86. package/src/code-vision.ts +3 -3
  87. package/src/completion.ts +15 -15
  88. package/src/context-menu-rows.ts +3 -2
  89. package/src/definition-link.ts +17 -5
  90. package/src/document-colors.ts +189 -0
  91. package/src/document-links.ts +129 -0
  92. package/src/document-query.ts +71 -0
  93. package/src/edit-locations.ts +162 -0
  94. package/src/editor-language.ts +80 -3
  95. package/src/emmet-module.d.ts +44 -0
  96. package/src/emmet.ts +73 -0
  97. package/src/folding.ts +3 -3
  98. package/src/format-on-type.ts +5 -5
  99. package/src/highlights.ts +3 -3
  100. package/src/history.ts +81 -1
  101. package/src/host-types.ts +10 -0
  102. package/src/index.ts +12 -3
  103. package/src/inlay-hints.ts +2 -2
  104. package/src/inline-completion.ts +221 -0
  105. package/src/key-priority.ts +9 -6
  106. package/src/linked-editing.ts +199 -0
  107. package/src/locales/en.json +31 -2
  108. package/src/locales/nl.json +31 -2
  109. package/src/markup-context.ts +262 -0
  110. package/src/markup.ts +367 -0
  111. package/src/navigation.ts +6 -6
  112. package/src/paste-history.ts +111 -0
  113. package/src/peek.ts +4 -4
  114. package/src/pick.ts +201 -19
  115. package/src/popups.ts +19 -1
  116. package/src/project-language.ts +136 -19
  117. package/src/rename.ts +11 -11
  118. package/src/rendered-docs.ts +2 -2
  119. package/src/selection-ranges.ts +3 -3
  120. package/src/signature.ts +6 -6
  121. package/src/snippet-session.ts +9 -4
  122. package/src/symbol-picker.ts +6 -6
  123. package/src/symbols.ts +30 -4
  124. package/src/testing.ts +126 -1
package/src/markup.ts ADDED
@@ -0,0 +1,367 @@
1
+ import type { EditorApplyEditsOptions, EditorContentChange, EditorRange, EditorTextChange, EditorTrackedRange } from '@adecore/editor';
2
+ import { abbreviationBefore, expandAbbreviation } from './emmet.ts';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { KEY_PRIORITY } from './key-priority.ts';
5
+ import { lineTextOf } from './line-text.ts';
6
+ import { isVoidElement, markupPlaceAt, markupSyntaxOf, tagPairAt, type MarkupSyntax } from './markup-context.ts';
7
+ import { isShortcut } from './shortcut-keys.ts';
8
+ import { parseSnippet } from './snippet.ts';
9
+ import { indentSnippet } from './snippet-indent.ts';
10
+ import { positionInInsertion } from './snippet-session.ts';
11
+
12
+ /* Each is on unless false. */
13
+ export interface MarkupSettings {
14
+ /* Tab after an abbreviation expands it, as Emmet does. */
15
+ readonly emmet?: boolean;
16
+ /* The `>` of a start tag adds its end tag after the caret, and `</` completes the end tag of the open element. */
17
+ readonly closeTags?: boolean;
18
+ /* Changing the name of a start tag changes the name of its end tag with it, and the other way around. */
19
+ readonly linkedTags?: boolean;
20
+ }
21
+
22
+ interface Span {
23
+ readonly from: number;
24
+ readonly to: number;
25
+ }
26
+
27
+ /* The tag name the caret is in and the name of its other tag, as offsets of the current text. */
28
+ interface Link {
29
+ readonly own: Span;
30
+ readonly partner: Span;
31
+ }
32
+
33
+ const NAME = /^[\w:.-]*$/;
34
+ const NAME_BEFORE = /<\/?[\w:.-]*$/;
35
+
36
+ /*
37
+ * Writing HTML, XML, the template of a Vue file and the markup of PHP: Emmet abbreviations, end tags that
38
+ * write themselves and tag names that change in pairs. The edits it adds join the undo step of the key that
39
+ * caused them. It does nothing in other languages, in a document over `MAX_MARKUP_SCAN` characters, or with more
40
+ * than one caret.
41
+ */
42
+ export class MarkupFeature {
43
+ private readonly language: EditorLanguage;
44
+ private readonly syntax: MarkupSyntax | null;
45
+ private settings: Required<MarkupSettings> = { emmet: true, closeTags: true, linkedTags: true };
46
+ private link: Link | null = null;
47
+ private wrapping: EditorTrackedRange | null = null;
48
+ /* Raised by every change of the text, so a follow-up edit for an older text is dropped. */
49
+ private revision = 0;
50
+ private mirroring = false;
51
+
52
+ constructor(language: EditorLanguage) {
53
+ this.language = language;
54
+ this.syntax = markupSyntaxOf(language.languageId);
55
+ if (this.syntax === null) {
56
+ return;
57
+ }
58
+ const { editor } = language;
59
+ const offs = [
60
+ language.onKey((event) => this.key(event), KEY_PRIORITY.markup),
61
+ editor.onTextChange((change) => this.edited(change)),
62
+ editor.onCaret(() => this.moved())
63
+ ];
64
+ language.onDispose(() => {
65
+ this.cancelWrap();
66
+ for (const off of offs) {
67
+ off();
68
+ }
69
+ });
70
+ }
71
+
72
+ /* Whether the language of the document is markup this feature writes. */
73
+ get supported(): boolean {
74
+ return this.syntax !== null;
75
+ }
76
+
77
+ configure(settings: MarkupSettings): void {
78
+ this.settings = {
79
+ emmet: settings.emmet !== false,
80
+ closeTags: settings.closeTags !== false,
81
+ linkedTags: settings.linkedTags !== false
82
+ };
83
+ if (!this.settings.linkedTags) {
84
+ this.link = null;
85
+ }
86
+ }
87
+
88
+ /* Expands the abbreviation before the caret, its places to fill as a snippet; false when there is none, which leaves Tab its own meaning. */
89
+ expand(): boolean {
90
+ const { syntax } = this;
91
+ const { editor } = this.language;
92
+ if (syntax === null || !this.settings.emmet || !this.editable()) {
93
+ return false;
94
+ }
95
+ const caret = editor.getCaret();
96
+ const line = lineTextOf(editor, caret.line);
97
+ const found = abbreviationBefore(line, caret.character);
98
+ if (found === null) {
99
+ return false;
100
+ }
101
+ const range = { start: { line: caret.line, character: found.start }, end: { line: caret.line, character: found.end } };
102
+ if (markupPlaceAt(editor.getText(), editor.offsetAt(range.start), syntax)?.kind !== 'text') {
103
+ return false;
104
+ }
105
+ const expanded = expandAbbreviation(found.abbreviation, { syntax, indent: this.indent() });
106
+ return expanded !== null && this.insert(range, expanded, 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(): boolean {
111
+ const { editor, popups } = this.language;
112
+ if (this.syntax === null || !this.editable(true)) {
113
+ return false;
114
+ }
115
+ const range = this.wrapTarget();
116
+ if (range === null) {
117
+ return false;
118
+ }
119
+ this.cancelWrap();
120
+ this.wrapping = editor.trackRange(range);
121
+ popups.setState({ wrap: { anchor: range.start, error: null } });
122
+ return true;
123
+ }
124
+
125
+ /* Wraps the text the field was opened over; false, with the reason in the field, when the abbreviation does not expand. */
126
+ submitWrap(abbreviation: string): boolean {
127
+ const { syntax } = this;
128
+ const { editor, popups, project } = this.language;
129
+ const range = this.wrapping?.get() ?? null;
130
+ if (syntax === null || range === null) {
131
+ this.cancelWrap();
132
+ return false;
133
+ }
134
+ const expanded = expandAbbreviation(abbreviation.trim(), { syntax, indent: this.indent(), wrap: this.wrapLines(range) });
135
+ if (expanded === null) {
136
+ popups.setState({ wrap: { anchor: range.start, error: project.i18n.t('editor:language.markup.invalid') } });
137
+ return false;
138
+ }
139
+ this.cancelWrap();
140
+ const inserted = this.insert(range, expanded, lineTextOf(editor, range.start.line));
141
+ editor.focus();
142
+ return inserted;
143
+ }
144
+
145
+ cancelWrap(): void {
146
+ this.wrapping?.dispose();
147
+ this.wrapping = null;
148
+ if (this.language.popups.getState().wrap !== null) {
149
+ this.language.popups.setState({ wrap: null });
150
+ }
151
+ }
152
+
153
+ private key(event: KeyboardEvent): boolean {
154
+ if (isShortcut(this.language.shortcuts.wrapWithAbbreviation, event)) {
155
+ return this.startWrap();
156
+ }
157
+ if (event.key !== 'Tab' || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || event.isComposing) {
158
+ return false;
159
+ }
160
+ return this.expand();
161
+ }
162
+
163
+ private editable(selection = false): boolean {
164
+ const { editor } = this.language;
165
+ const selections = editor.getSelections();
166
+ if (editor.isReadOnly?.() === true || selections.length !== 1) {
167
+ return false;
168
+ }
169
+ const { start, end } = selections[0]!;
170
+ return selection || (start.line === end.line && start.character === end.character);
171
+ }
172
+
173
+ private indent(): string {
174
+ const { tabSize, insertSpaces } = this.language.editor.getIndentation();
175
+ return insertSpaces ? ' '.repeat(tabSize) : '\t';
176
+ }
177
+
178
+ /* The selection without the whitespace around it, or the text of the caret's line; null when that is blank. */
179
+ private wrapTarget(): EditorRange | null {
180
+ const { editor } = this.language;
181
+ const selection = editor.getSelection();
182
+ const text = editor.getText();
183
+ const empty = selection.start.line === selection.end.line && selection.start.character === selection.end.character;
184
+ let from = empty ? editor.offsetAt({ line: selection.start.line, character: 0 }) : editor.offsetAt(selection.start);
185
+ let to = empty ? from + lineTextOf(editor, selection.start.line).length : editor.offsetAt(selection.end);
186
+ while (from < to && /\s/.test(text[from]!)) {
187
+ from++;
188
+ }
189
+ while (to > from && /\s/.test(text[to - 1]!)) {
190
+ to--;
191
+ }
192
+ return from === to ? null : { start: editor.positionAt(from), end: editor.positionAt(to) };
193
+ }
194
+
195
+ /* The lines of the wrapped text, without the indentation they share, which the expansion gives them again. */
196
+ private wrapLines(range: EditorRange): string[] {
197
+ const lines = this.language.editor.textInRange(range).split(/\r\n|\r|\n/);
198
+ const indents = lines.slice(1).flatMap((line) => (line.trim() === '' ? [] : [/^[\t ]*/.exec(line)![0].length]));
199
+ const shared = indents.length === 0 ? 0 : Math.min(...indents);
200
+ return lines.map((line, index) => (index === 0 ? line : line.slice(Math.min(shared, /^[\t ]*/.exec(line)![0].length))));
201
+ }
202
+
203
+ private insert(range: EditorRange, snippet: string, line: string): boolean {
204
+ const { editor, snippets } = this.language;
205
+ const parsed = indentSnippet(parseSnippet(snippet), line);
206
+ if (parsed.limited === true) {
207
+ return false;
208
+ }
209
+ if (!this.apply([{ range, text: parsed.text }])) {
210
+ return false;
211
+ }
212
+ if (parsed.stops.length > 0) {
213
+ snippets.begin(range.start, parsed.text, parsed.stops, parsed.fields);
214
+ } else {
215
+ editor.setCaret(positionInInsertion(range.start, parsed.text, parsed.text.length));
216
+ }
217
+ return true;
218
+ }
219
+
220
+ /* Through the snippet session while one runs, so an edit inside its field keeps it going. */
221
+ private apply(changes: readonly EditorContentChange[], options?: EditorApplyEditsOptions): boolean {
222
+ const { editor, snippets } = this.language;
223
+ return snippets.isActive ? snippets.applyEdits(changes, snippets.token, [], options) : editor.applyEdits(changes, options);
224
+ }
225
+
226
+ private edited(change: EditorTextChange): void {
227
+ const revision = ++this.revision;
228
+ if (this.mirroring) {
229
+ return;
230
+ }
231
+ const edit = change.changes[0];
232
+ // Backspace and the word deletes are commands too; an undo of both names is two changes and lets go.
233
+ if (change.source === 'external' || change.changes.length !== 1 || edit === undefined || edit.range.start.line !== edit.range.end.line) {
234
+ this.link = null;
235
+ return;
236
+ }
237
+ const from = this.language.editor.offsetAt(edit.range.start);
238
+ const removed = edit.range.end.character - edit.range.start.character;
239
+ this.follow(from, removed, edit.text, revision);
240
+ if (change.source !== 'input' || !this.settings.closeTags || removed !== 0 || this.language.editor.getSelections().length !== 1) {
241
+ return;
242
+ }
243
+ if (edit.text === '>') {
244
+ queueMicrotask(() => this.closeTag(from, revision));
245
+ } else if (edit.text === '/') {
246
+ queueMicrotask(() => this.completeEndTag(from, revision));
247
+ }
248
+ }
249
+
250
+ /* Keeps the link on the name being typed and copies the name to its other tag once the edit is in. */
251
+ private follow(from: number, removed: number, inserted: string, revision: number): void {
252
+ const link = this.link;
253
+ if (link === null) {
254
+ return;
255
+ }
256
+ const { own, partner } = link;
257
+ if (from < own.from || from + removed > own.to || !NAME.test(inserted)) {
258
+ this.link = null;
259
+ return;
260
+ }
261
+ const delta = inserted.length - removed;
262
+ this.link = {
263
+ own: { from: own.from, to: own.to + delta },
264
+ partner: partner.from > own.from ? { from: partner.from + delta, to: partner.to + delta } : partner
265
+ };
266
+ queueMicrotask(() => this.mirror(revision));
267
+ }
268
+
269
+ private mirror(revision: number): void {
270
+ const link = this.link;
271
+ if (link === null || revision !== this.revision) {
272
+ return;
273
+ }
274
+ const { editor } = this.language;
275
+ const text = editor.getText();
276
+ const name = text.slice(link.own.from, link.own.to);
277
+ if (name === text.slice(link.partner.from, link.partner.to)) {
278
+ return;
279
+ }
280
+ const range = { start: editor.positionAt(link.partner.from), end: editor.positionAt(link.partner.to) };
281
+ this.mirroring = true;
282
+ try {
283
+ if (!this.apply([{ range, text: name }], { mergeUndo: true })) {
284
+ this.link = null;
285
+ return;
286
+ }
287
+ } finally {
288
+ this.mirroring = false;
289
+ }
290
+ const delta = name.length - (link.partner.to - link.partner.from);
291
+ this.link = {
292
+ own: link.partner.from < link.own.from ? { from: link.own.from + delta, to: link.own.to + delta } : link.own,
293
+ partner: { from: link.partner.from, to: link.partner.to + delta }
294
+ };
295
+ }
296
+
297
+ /* The `>` at `at` ended a start tag: its end tag goes after it, with the caret between the two. */
298
+ private closeTag(at: number, revision: number): void {
299
+ const { editor } = this.language;
300
+ const text = editor.getText();
301
+ if (revision !== this.revision || text[at] !== '>' || text[at - 1] === '/') {
302
+ return;
303
+ }
304
+ const place = markupPlaceAt(text, at, this.syntax!);
305
+ if (place?.kind !== 'tag' || place.tag.closing || place.tag.inValue || place.tag.name === '' || isVoidElement(place.tag.name, this.syntax!)) {
306
+ return;
307
+ }
308
+ const end = `</${place.tag.name}>`;
309
+ if (text.startsWith(end, at + 1)) {
310
+ return;
311
+ }
312
+ const caret = editor.positionAt(at + 1);
313
+ this.apply([{ range: { start: caret, end: caret }, text: end }], { mergeUndo: true, mapSelections: () => ({ start: caret, end: caret }) });
314
+ }
315
+
316
+ /* The `/` at `at` follows a `<` in text: the end tag of the innermost open element is written out. */
317
+ private completeEndTag(at: number, revision: number): void {
318
+ const { editor } = this.language;
319
+ const text = editor.getText();
320
+ if (revision !== this.revision || text[at] !== '/' || text[at - 1] !== '<') {
321
+ return;
322
+ }
323
+ const place = markupPlaceAt(text, at - 1, this.syntax!);
324
+ const element = place?.kind === 'text' ? place.open.at(-1) : undefined;
325
+ if (element === undefined) {
326
+ return;
327
+ }
328
+ const position = editor.positionAt(at + 1);
329
+ const closed = text[at + 1] === '>';
330
+ const completion = closed ? element.name : `${element.name}>`;
331
+ const after = { line: position.line, character: position.character + element.name.length + 1 };
332
+ this.apply([{ range: { start: position, end: position }, text: completion }], { mergeUndo: true, mapSelections: () => ({ start: after, end: after }) });
333
+ }
334
+
335
+ /* Links the tag name the caret moved into with its other tag, once the edit that moved it is in. */
336
+ private moved(): void {
337
+ if (!this.settings.linkedTags || this.mirroring) {
338
+ return;
339
+ }
340
+ queueMicrotask(() => {
341
+ this.link = this.linkAtCaret();
342
+ });
343
+ }
344
+
345
+ private linkAtCaret(): Link | null {
346
+ const { editor } = this.language;
347
+ if (!this.editable()) {
348
+ return null;
349
+ }
350
+ const caret = editor.getCaret();
351
+ const offset = editor.offsetAt(caret);
352
+ const link = this.link;
353
+ if (link !== null && link.own.from <= offset && offset <= link.own.to) {
354
+ return link;
355
+ }
356
+ const line = lineTextOf(editor, caret.line);
357
+ if (!NAME_BEFORE.test(line.slice(0, caret.character))) {
358
+ return null;
359
+ }
360
+ const pair = tagPairAt(editor.getText(), offset, this.syntax!);
361
+ if (pair === null) {
362
+ return null;
363
+ }
364
+ const inOpen = pair.open.from <= offset && offset <= pair.open.to;
365
+ return inOpen ? { own: pair.open, partner: pair.close } : { own: pair.close, partner: pair.open };
366
+ }
367
+ }
package/src/navigation.ts CHANGED
@@ -95,11 +95,11 @@ export class NavigationFeature {
95
95
  }
96
96
 
97
97
  supports(kind: NavigationKind): boolean {
98
- return this.language.project.service.supports(METHODS[kind], this.language.uri);
98
+ return this.language.service.supports(METHODS[kind], this.language.uri);
99
99
  }
100
100
 
101
101
  async go(kind: NavigationKind, position: EditorPosition = this.language.editor.getCaret()): Promise<void> {
102
- const { project, uri, editor } = this.language;
102
+ const { uri, editor } = this.language;
103
103
 
104
104
  if (!this.supports(kind)) {
105
105
  this.tell(this.say('unavailable'));
@@ -110,7 +110,7 @@ export class NavigationFeature {
110
110
  let result: NavigationResult;
111
111
 
112
112
  try {
113
- result = await REQUESTS[kind](project.service, uri, position);
113
+ result = await REQUESTS[kind](this.language.service, uri, position);
114
114
  } catch (error) {
115
115
  if (!(error instanceof StaleResultError)) {
116
116
  this.tell(this.say('failed', { message: messageOf(error) }));
@@ -141,7 +141,7 @@ export class NavigationFeature {
141
141
  * up by name among the symbols of the project. One match goes there at once, several are listed, and none says nothing.
142
142
  */
143
143
  async goToName(name: string, anchor: EditorPosition, own: Location | null): Promise<void> {
144
- const { project, uri, editor } = this.language;
144
+ const { uri, editor } = this.language;
145
145
 
146
146
  if (own !== null) {
147
147
  this.language.goTo(own);
@@ -149,12 +149,12 @@ export class NavigationFeature {
149
149
  return;
150
150
  }
151
151
 
152
- if (!project.service.supports('workspace/symbol', uri)) {
152
+ if (!this.language.service.supports('workspace/symbol', uri)) {
153
153
  return;
154
154
  }
155
155
 
156
156
  try {
157
- const places = placesOfName(await project.service.workspaceSymbols(uri, name), name, editor.getText());
157
+ const places = placesOfName(await this.language.service.workspaceSymbols(uri, name), name, editor.getText());
158
158
 
159
159
  if (places.length === 1) {
160
160
  this.language.goTo(places[0]!);
@@ -0,0 +1,111 @@
1
+ import { rememberCopy } from '@adecore/editor';
2
+ import type { ClipboardEntry } from './clipboard-history.ts';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { KEY_PRIORITY } from './key-priority.ts';
5
+ import type { PickGroup } from './popups.ts';
6
+ import { isShortcut } from './shortcut-keys.ts';
7
+
8
+ const LABEL_LENGTH = 80;
9
+
10
+ /* The first characters of a copy on one line, with each line break shown as a return sign. */
11
+ function labelOf(text: string): string {
12
+ const flat = text.replace(/\r\n|\r|\n/g, '⏎');
13
+
14
+ return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH)}…` : flat;
15
+ }
16
+
17
+ /*
18
+ * Paste from History: a list of what was copied in the editors of the project, newest first. Typing narrows it,
19
+ * Shift with the arrows chooses several entries, which paste as one, and Delete forgets entries.
20
+ */
21
+ export class PasteHistoryFeature {
22
+ private readonly language: EditorLanguage;
23
+
24
+ constructor(language: EditorLanguage) {
25
+ this.language = language;
26
+
27
+ const off = language.onKey((event) => {
28
+ if (!isShortcut(this.language.shortcuts.pasteFromHistory, event)) {
29
+ return false;
30
+ }
31
+
32
+ this.open();
33
+
34
+ return true;
35
+ }, KEY_PRIORITY.pasteHistory);
36
+
37
+ language.onDispose(off);
38
+ }
39
+
40
+ open(): void {
41
+ const { editor, project } = this.language;
42
+ const { clipboard } = project;
43
+
44
+ if (clipboard.entries.length === 0) {
45
+ editor.announce(project.i18n.t('editor:language.pasteHistory.empty'));
46
+
47
+ return;
48
+ }
49
+
50
+ const ids = new Map<ClipboardEntry, string>();
51
+ const entries = new Map<string, ClipboardEntry>();
52
+
53
+ const groups = (): readonly PickGroup[] => [
54
+ {
55
+ title: null,
56
+ rows: clipboard.entries.map((entry) => {
57
+ const id = ids.get(entry) ?? String(ids.size);
58
+
59
+ ids.set(entry, id);
60
+ entries.set(id, entry);
61
+
62
+ return { id, label: labelOf(entry.text), detail: '', search: entry.text };
63
+ })
64
+ }
65
+ ];
66
+
67
+ const rows = groups();
68
+
69
+ this.language.pick.open({
70
+ anchor: editor.getCaret(),
71
+ title: project.i18n.t('editor:language.pasteHistory.title'),
72
+ groups: rows,
73
+ active: rows[0]!.rows[0]!.id,
74
+ filter: true,
75
+ multiple: true,
76
+ accept: (id) => this.paste([entries.get(id)!]),
77
+ acceptMany: (chosen) => this.paste(clipboard.entries.filter((entry) => chosen.includes(ids.get(entry) ?? ''))),
78
+ preview: async (id) => ({ removed: [], added: entries.get(id)?.text.split('\n') ?? [], note: null, kind: 'text' }),
79
+ remove: (chosen) => {
80
+ for (const id of chosen) {
81
+ clipboard.remove(entries.get(id)!);
82
+ }
83
+
84
+ return groups();
85
+ }
86
+ });
87
+ }
88
+
89
+ /* Pastes entries in list order, joined by line breaks, as a paste of that text from the clipboard would. */
90
+ private paste(chosen: readonly ClipboardEntry[]): void {
91
+ const { editor, project } = this.language;
92
+ const { clipboard } = project;
93
+
94
+ if (chosen.length === 0) {
95
+ return;
96
+ }
97
+
98
+ const text = chosen.map((entry) => entry.text).join('\n');
99
+ const wholeLines = chosen.length === 1 && chosen[0]!.wholeLines;
100
+
101
+ if (chosen.length === 1) {
102
+ clipboard.promote(chosen[0]!);
103
+ } else {
104
+ clipboard.add(text);
105
+ }
106
+
107
+ globalThis.navigator?.clipboard?.writeText(text).catch(() => undefined);
108
+ rememberCopy(text, wholeLines);
109
+ editor.paste?.(text, { wholeLines });
110
+ }
111
+ }
package/src/peek.ts CHANGED
@@ -82,7 +82,7 @@ export class PeekFeature {
82
82
  const { editor, project, uri } = this.language;
83
83
  const method = KINDS[kind].method;
84
84
 
85
- if (!project.service.supports(method, uri)) {
85
+ if (!this.language.service.supports(method, uri)) {
86
86
  this.tell(this.say(KINDS[kind].unavailable));
87
87
 
88
88
  return;
@@ -160,14 +160,14 @@ export class PeekFeature {
160
160
  }
161
161
 
162
162
  private async fetch(kind: PeekKind, position: EditorPosition): Promise<{ locations: Location[]; targets: Map<string, Range> }> {
163
- const { project, uri } = this.language;
163
+ const { uri } = this.language;
164
164
 
165
165
  if (kind === 'references') {
166
166
  // Without the declaration, so the list holds what the usages count above a declaration counts.
167
- return { locations: (await project.service.references(uri, position, false)) ?? [], targets: new Map() };
167
+ return { locations: (await this.language.service.references(uri, position, false)) ?? [], targets: new Map() };
168
168
  }
169
169
 
170
- const places = uniquePlaces(await project.service.definition(uri, position));
170
+ const places = uniquePlaces(await this.language.service.definition(uri, position));
171
171
  const targets = new Map(places.flatMap(({ location, target }) => (target === null ? [] : [[placeKey(location), target] as const])));
172
172
 
173
173
  return { locations: places.map(({ location }) => location), targets };