@adecore/editor-react 0.42.0 → 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 (122) 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 +190 -70
  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.js +10 -15
  17. package/dist/context-menu-rows.d.ts +1 -1
  18. package/dist/context-menu-rows.js +4 -0
  19. package/dist/definition-link.js +15 -5
  20. package/dist/document-colors.d.ts +24 -0
  21. package/dist/document-colors.js +194 -0
  22. package/dist/document-links.d.ts +22 -0
  23. package/dist/document-links.js +133 -0
  24. package/dist/document-query.d.ts +15 -0
  25. package/dist/document-query.js +54 -0
  26. package/dist/edit-locations.d.ts +20 -0
  27. package/dist/edit-locations.js +136 -0
  28. package/dist/editor-language.d.ts +19 -1
  29. package/dist/editor-language.js +77 -2
  30. package/dist/emmet.d.ts +13 -0
  31. package/dist/emmet.js +62 -0
  32. package/dist/folding.js +2 -3
  33. package/dist/format-on-type.js +3 -5
  34. package/dist/highlights.js +2 -3
  35. package/dist/history.d.ts +7 -0
  36. package/dist/history.js +79 -1
  37. package/dist/host-types.d.ts +6 -0
  38. package/dist/index.d.ts +11 -2
  39. package/dist/index.js +8 -0
  40. package/dist/inlay-hints.js +2 -2
  41. package/dist/inline-completion.d.ts +39 -0
  42. package/dist/inline-completion.js +221 -0
  43. package/dist/key-priority.d.ts +8 -5
  44. package/dist/key-priority.js +9 -6
  45. package/dist/linked-editing.d.ts +26 -0
  46. package/dist/linked-editing.js +215 -0
  47. package/dist/locales/en.json +31 -2
  48. package/dist/locales/nl.json +31 -2
  49. package/dist/markup-context.d.ts +37 -0
  50. package/dist/markup-context.js +291 -0
  51. package/dist/markup.d.ts +36 -0
  52. package/dist/markup.js +453 -0
  53. package/dist/navigation.js +4 -6
  54. package/dist/paste-history.d.ts +7 -0
  55. package/dist/paste-history.js +104 -0
  56. package/dist/peek.js +3 -4
  57. package/dist/pick.d.ts +20 -2
  58. package/dist/pick.js +141 -18
  59. package/dist/popups.d.ts +9 -0
  60. package/dist/popups.js +2 -1
  61. package/dist/project-language.d.ts +18 -1
  62. package/dist/project-language.js +129 -18
  63. package/dist/rename.js +8 -11
  64. package/dist/rendered-docs.js +1 -2
  65. package/dist/selection-ranges.js +2 -3
  66. package/dist/shortcut-keys.d.ts +19 -0
  67. package/dist/signature.js +3 -6
  68. package/dist/snippet-session.d.ts +2 -2
  69. package/dist/snippet-session.js +3 -3
  70. package/dist/symbol-picker.js +4 -6
  71. package/dist/symbols.js +22 -3
  72. package/dist/testing.d.ts +23 -1
  73. package/dist/testing.js +114 -0
  74. package/package.json +7 -6
  75. package/src/CallHierarchyPanel.tsx +26 -11
  76. package/src/EditorContextMenu.tsx +7 -0
  77. package/src/LanguagePopups.tsx +4 -0
  78. package/src/PickPopup.tsx +40 -5
  79. package/src/WrapCard.tsx +64 -0
  80. package/src/call-hierarchy-model.ts +25 -3
  81. package/src/call-hierarchy.ts +62 -20
  82. package/src/clipboard-history.ts +102 -0
  83. package/src/code-actions.ts +48 -11
  84. package/src/code-vision.ts +3 -3
  85. package/src/completion.ts +15 -15
  86. package/src/context-menu-rows.ts +3 -2
  87. package/src/definition-link.ts +17 -5
  88. package/src/document-colors.ts +189 -0
  89. package/src/document-links.ts +129 -0
  90. package/src/document-query.ts +71 -0
  91. package/src/edit-locations.ts +162 -0
  92. package/src/editor-language.ts +80 -3
  93. package/src/emmet-module.d.ts +44 -0
  94. package/src/emmet.ts +73 -0
  95. package/src/folding.ts +3 -3
  96. package/src/format-on-type.ts +5 -5
  97. package/src/highlights.ts +3 -3
  98. package/src/history.ts +81 -1
  99. package/src/host-types.ts +10 -0
  100. package/src/index.ts +12 -3
  101. package/src/inlay-hints.ts +2 -2
  102. package/src/inline-completion.ts +221 -0
  103. package/src/key-priority.ts +9 -6
  104. package/src/linked-editing.ts +199 -0
  105. package/src/locales/en.json +31 -2
  106. package/src/locales/nl.json +31 -2
  107. package/src/markup-context.ts +262 -0
  108. package/src/markup.ts +367 -0
  109. package/src/navigation.ts +6 -6
  110. package/src/paste-history.ts +111 -0
  111. package/src/peek.ts +4 -4
  112. package/src/pick.ts +201 -19
  113. package/src/popups.ts +19 -1
  114. package/src/project-language.ts +136 -19
  115. package/src/rename.ts +11 -11
  116. package/src/rendered-docs.ts +2 -2
  117. package/src/selection-ranges.ts +3 -3
  118. package/src/signature.ts +6 -6
  119. package/src/snippet-session.ts +9 -4
  120. package/src/symbol-picker.ts +6 -6
  121. package/src/symbols.ts +30 -4
  122. package/src/testing.ts +126 -1
@@ -1,7 +1,20 @@
1
1
  import { disposeSnippetTransforms } from './snippet-transform-queue.ts';
2
2
  import i18next, { type i18n } from 'i18next';
3
- import { fileUriToPath, pathToFileUri, type ApplyWorkspaceEditResult, type ContentChange, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
3
+ import {
4
+ fileUriToPath,
5
+ pathToFileUri,
6
+ unservedLanguageService,
7
+ type ApplyWorkspaceEditResult,
8
+ type ContentChange,
9
+ type Disposable,
10
+ type LanguageService,
11
+ type ShowDocumentParams,
12
+ type ShowDocumentResult,
13
+ type WorkspaceEdit
14
+ } from '@adecore/lsp';
4
15
  import type { Editor, EditorPosition } from '@adecore/editor';
16
+ import { ClipboardHistory } from './clipboard-history.ts';
17
+ import { EditLocations } from './edit-locations.ts';
5
18
  import { NavigationHistory, type Place } from './navigation-history.ts';
6
19
  import { ProjectProblems, serviceProblems } from './project-problems.ts';
7
20
  import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
@@ -16,6 +29,8 @@ export interface LanguageDocumentHandle {
16
29
  release(): void;
17
30
  }
18
31
 
32
+ const UNSERVED = unservedLanguageService();
33
+
19
34
  /* Sends one editor's edits to the document, holding them until the document is open. */
20
35
  class DocumentSync {
21
36
  readonly ready: Promise<void>;
@@ -76,10 +91,18 @@ class DocumentSync {
76
91
 
77
92
  interface Holder {
78
93
  readonly editors: Editor[];
79
- sync: DocumentSync;
94
+ /* The editors a server serves, in the order they came; the first one sends the changes. */
95
+ readonly served: Editor[];
96
+ /* Null while no editor of the document is served, so nothing of it is open in the service. */
97
+ sync: DocumentSync | null;
80
98
  languageId: string;
81
99
  }
82
100
 
101
+ export interface AcquireOptions {
102
+ /* False registers the editor as open without telling the service, for a document no server serves; true by default. */
103
+ readonly serve?: boolean;
104
+ }
105
+
83
106
  /* A file or folder for a file operation notification: a bare path is a file. */
84
107
  export type FileOperationTarget = string | { readonly path: string; readonly directory?: boolean };
85
108
 
@@ -103,6 +126,8 @@ export class ProjectLanguage {
103
126
  readonly folder: string;
104
127
  readonly problems: ProjectProblems;
105
128
  readonly history = new NavigationHistory();
129
+ readonly edits: EditLocations;
130
+ readonly clipboard: ClipboardHistory;
106
131
  private readonly holders = new Map<string, Holder>();
107
132
  private readonly files: ProjectFiles | null;
108
133
 
@@ -113,6 +138,9 @@ export class ProjectLanguage {
113
138
  private pendingCaret: Place | null = null;
114
139
  private lastPlace: Place | null = null;
115
140
  private disposed = false;
141
+ private readonly subscriptions: readonly Disposable[];
142
+ /* The result id the server gave each document in its last workspace report, which the next asks with. */
143
+ private readonly workspaceResults = new Map<string, string>();
116
144
 
117
145
  constructor(service: LanguageService, host: LanguageHost = {}) {
118
146
  this.service = service;
@@ -120,8 +148,13 @@ export class ProjectLanguage {
120
148
  this.i18n = host.i18n ?? i18next;
121
149
  this.folder = host.folder ?? '';
122
150
  this.files = host.files ?? null;
151
+ this.edits = new EditLocations(host.editHistoryLimit);
152
+ this.clipboard = host.clipboard ?? new ClipboardHistory();
123
153
  this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
124
154
  this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
155
+ const shows = service.onShowDocument?.((params) => this.showDocument(params));
156
+ const messages = host.onServerMessage === undefined ? undefined : service.onServerMessage?.((message) => host.onServerMessage!(message));
157
+ this.subscriptions = [shows, messages].flatMap((subscription) => (subscription === undefined ? [] : [subscription]));
125
158
  }
126
159
 
127
160
  /* Open panels of other editors of the project follow a resize. */
@@ -182,20 +215,73 @@ export class ProjectLanguage {
182
215
 
183
216
  /* Tells the servers that files were deleted, once they are. */
184
217
  didDeleteFiles(targets: readonly FileOperationTarget[]): Promise<void> {
185
- return this.service.didDeleteFiles?.(filesOf(targets)) ?? Promise.resolve();
218
+ const files = filesOf(targets);
219
+
220
+ this.edits.forget((uri) => files.some((file) => (file.directory ? uri.startsWith(`${file.uri}/`) : uri === file.uri)));
221
+
222
+ return this.service.didDeleteFiles?.(files) ?? Promise.resolve();
186
223
  }
187
224
 
188
- /* The text of a file as it stands now: an open document's, else a draft's or the machine's; null when it is no text file. */
225
+ /*
226
+ * The text of a file as it stands now: an open document's, else a draft's or the machine's; null when it is no
227
+ * text file. A document of a scheme a server owns, such as a decompiled class, is read from that server.
228
+ */
189
229
  async readText(uri: string): Promise<string | null> {
190
- const open = this.holders.get(uri)?.sync.text;
230
+ const open = this.holders.get(uri)?.editors[0]?.getText();
191
231
 
192
232
  if (open !== undefined) {
193
233
  return open;
194
234
  }
195
235
 
196
236
  const path = fileUriToPath(uri);
237
+ if (path === null) {
238
+ return (await this.service.textDocumentContent?.(uri).catch(() => null)) ?? null;
239
+ }
197
240
 
198
- return path === null || this.files === null ? null : ((await this.files.read(path))?.text ?? null);
241
+ return this.files === null ? null : ((await this.files.read(path))?.text ?? null);
242
+ }
243
+
244
+ /*
245
+ * Asks the server of `uri` for the problems of every file it knows and lists them in `problems`, for a server that
246
+ * offers workspace diagnostics. A file the server reports as unchanged keeps what it had. False when no server offers them.
247
+ */
248
+ async pullWorkspaceProblems(uri: string, options: { readonly signal?: AbortSignal } = {}): Promise<boolean> {
249
+ const { service } = this;
250
+ if (service.workspaceDiagnostics === undefined || !service.supports('workspace/diagnostic', uri)) {
251
+ return false;
252
+ }
253
+ const previous = [...this.workspaceResults].map(([key, value]) => ({ uri: key, value }));
254
+ const report = await service.workspaceDiagnostics(uri, previous, options);
255
+ const server = this.host.serverNames?.(uri)[0] ?? 'workspace';
256
+ for (const item of report.items) {
257
+ if (item.resultId !== undefined) {
258
+ this.workspaceResults.set(item.uri, item.resultId);
259
+ }
260
+ if (item.kind === 'full') {
261
+ this.problems.report(this.pathOfLocation(item.uri) ?? item.uri, server, item.items);
262
+ }
263
+ }
264
+ return true;
265
+ }
266
+
267
+ /* Shows what a server asks for: a web address outside the editor, anything else as a place of the host. */
268
+ private async showDocument(params: ShowDocumentParams): Promise<ShowDocumentResult> {
269
+ const web = /^(https?|mailto):/i.test(params.uri);
270
+ if (params.external === true || web) {
271
+ if (this.host.openExternal !== undefined) {
272
+ this.host.openExternal(params.uri);
273
+ return { success: true };
274
+ }
275
+ if (typeof window === 'undefined') {
276
+ return { success: false };
277
+ }
278
+ return { success: window.open(params.uri, '_blank', 'noopener,noreferrer') !== null };
279
+ }
280
+ if (this.host.openPlace === undefined) {
281
+ return { success: false };
282
+ }
283
+ this.openPlace({ uri: params.uri, position: params.selection?.start ?? { line: 0, character: 0 } });
284
+ return { success: true };
199
285
  }
200
286
 
201
287
  /*
@@ -234,27 +320,39 @@ export class ProjectLanguage {
234
320
  return pending.position;
235
321
  }
236
322
 
323
+ /* The editors that hold a document, one per uri in the order the documents were first opened. */
324
+ openEditors(): readonly { uri: string; editor: Editor }[] {
325
+ return [...this.holders].flatMap(([uri, holder]) => (holder.editors[0] === undefined ? [] : [{ uri, editor: holder.editors[0] }]));
326
+ }
327
+
237
328
  /* Opens `uri` for the editor, or joins the editors that already hold it. */
238
- acquire(uri: string, languageId: string, editor: Editor): LanguageDocumentHandle {
329
+ acquire(uri: string, languageId: string, editor: Editor, options: AcquireOptions = {}): LanguageDocumentHandle {
239
330
  if (this.disposed) {
240
331
  throw new Error('The project language is disposed');
241
332
  }
242
333
 
334
+ const serve = options.serve ?? true;
243
335
  let holder = this.holders.get(uri);
244
336
 
245
337
  if (holder === undefined) {
246
- holder = { editors: [], sync: new DocumentSync(this.service, uri, languageId, editor), languageId };
338
+ holder = { editors: [], served: [], sync: null, languageId };
247
339
  this.holders.set(uri, holder);
248
340
  }
249
341
 
250
342
  holder.editors.push(editor);
343
+
344
+ if (serve) {
345
+ holder.served.push(editor);
346
+ holder.sync ??= new DocumentSync(this.service, uri, languageId, editor);
347
+ }
348
+
251
349
  let released = false;
252
350
 
253
351
  return {
254
352
  uri,
255
- service: this.service,
353
+ service: serve ? this.service : UNSERVED,
256
354
  get ready() {
257
- return holder.sync.ready;
355
+ return serve ? (holder.sync?.ready ?? Promise.resolve()) : Promise.resolve();
258
356
  },
259
357
  release: () => {
260
358
  if (!released) {
@@ -273,26 +371,38 @@ export class ProjectLanguage {
273
371
  }
274
372
 
275
373
  holder.editors.splice(holder.editors.indexOf(editor), 1);
276
- const next = holder.editors[0];
277
374
 
278
- if (next === undefined) {
279
- holder.sync.dispose();
375
+ if (holder.served.includes(editor)) {
376
+ holder.served.splice(holder.served.indexOf(editor), 1);
377
+ }
378
+
379
+ const { sync } = holder;
380
+
381
+ if (holder.editors.length === 0) {
280
382
  this.holders.delete(uri);
383
+ }
281
384
 
282
- void holder.sync.ready
385
+ if (sync === null || sync.editor !== editor) {
386
+ return;
387
+ }
388
+
389
+ sync.dispose();
390
+ const next = holder.served[0];
391
+
392
+ // The editor that sent the changes is gone; the next served one takes over and opens the document with its own text.
393
+ holder.sync = next === undefined ? null : new DocumentSync(this.service, uri, holder.languageId, next);
394
+
395
+ if (holder.sync === null) {
396
+ void sync.ready
283
397
  .catch(() => undefined)
284
398
  .then(() => {
285
- if (this.holders.has(uri)) {
399
+ if (this.holders.get(uri)?.sync) {
286
400
  return undefined;
287
401
  }
288
402
 
289
403
  return this.service.closeDocument(uri);
290
404
  })
291
405
  .catch(() => undefined);
292
- } else if (holder.sync.editor === editor) {
293
- // The editor that sent the changes is gone; the next one takes over and opens the document with its own text.
294
- holder.sync.dispose();
295
- holder.sync = new DocumentSync(this.service, uri, holder.languageId, next);
296
406
  }
297
407
  }
298
408
 
@@ -302,9 +412,16 @@ export class ProjectLanguage {
302
412
  }
303
413
 
304
414
  this.disposed = true;
415
+ for (const subscription of this.subscriptions) {
416
+ subscription.dispose();
417
+ }
305
418
  disposeSnippetTransforms(this);
306
419
 
307
420
  for (const [uri, holder] of this.holders) {
421
+ if (holder.sync === null) {
422
+ continue;
423
+ }
424
+
308
425
  holder.sync.dispose();
309
426
 
310
427
  void holder.sync.ready
package/src/rename.ts CHANGED
@@ -67,9 +67,9 @@ export class RenameFeature {
67
67
  }
68
68
 
69
69
  async start(): Promise<void> {
70
- const { editor, project, uri } = this.language;
70
+ const { editor, uri } = this.language;
71
71
 
72
- if (!project.service.supports(METHOD, uri)) {
72
+ if (!this.language.service.supports(METHOD, uri)) {
73
73
  this.tell('error', this.say('unavailable'));
74
74
 
75
75
  return;
@@ -77,11 +77,11 @@ export class RenameFeature {
77
77
 
78
78
  this.cancel();
79
79
  const caret = editor.getCaret();
80
- const prepare = project.service.providerOptions(METHOD, uri)?.prepareProvider === true;
80
+ const prepare = this.language.service.providerOptions(METHOD, uri)?.prepareProvider === true;
81
81
  let answer: PrepareRenameResult;
82
82
 
83
83
  try {
84
- answer = prepare ? await project.service.prepareRename(uri, caret) : { defaultBehavior: true };
84
+ answer = prepare ? await this.language.service.prepareRename(uri, caret) : { defaultBehavior: true };
85
85
  } catch (error) {
86
86
  if (!(error instanceof StaleResultError)) {
87
87
  this.tell('error', this.say('nothing'));
@@ -127,7 +127,7 @@ export class RenameFeature {
127
127
  const { project, uri } = this.language;
128
128
 
129
129
  try {
130
- const edit = await project.service.rename(uri, session.target.range.start, next);
130
+ const edit = await this.language.service.rename(uri, session.target.range.start, next);
131
131
 
132
132
  if (this.session !== session) {
133
133
  return;
@@ -218,14 +218,14 @@ export class RenameFeature {
218
218
 
219
219
  /* Lights up the places the name has in this file, and counts them all; what the servers listed, or null when they did not. */
220
220
  private async light(session: Session): Promise<readonly Location[] | null> {
221
- const { editor, project, uri } = this.language;
221
+ const { editor, uri } = this.language;
222
222
 
223
- if (!project.service.supports('textDocument/references', uri)) {
223
+ if (!this.language.service.supports('textDocument/references', uri)) {
224
224
  return null;
225
225
  }
226
226
 
227
227
  try {
228
- const locations = await project.service.references(uri, session.target.range.start, true);
228
+ const locations = await this.language.service.references(uri, session.target.range.start, true);
229
229
 
230
230
  if (this.session !== session || locations === null) {
231
231
  return null;
@@ -277,14 +277,14 @@ export class RenameFeature {
277
277
 
278
278
  /* So a suggester knows a function from a variable; a rename on a use finds the kind through the declaration's place in this file. */
279
279
  private async kindOf(session: Session, locations: readonly Location[]): Promise<string | undefined> {
280
- const { project, uri } = this.language;
280
+ const { uri } = this.language;
281
281
 
282
- if (!project.service.supports('textDocument/documentSymbol', uri)) {
282
+ if (!this.language.service.supports('textDocument/documentSymbol', uri)) {
283
283
  return undefined;
284
284
  }
285
285
 
286
286
  try {
287
- const symbols = await project.service.documentSymbols(uri, { signal: session.names.signal, parallel: true });
287
+ const symbols = await this.language.service.documentSymbols(uri, { signal: session.names.signal, parallel: true });
288
288
 
289
289
  return renameKindOf(symbols, session.original, [session.target.range, ...occurrencesOf(locations, uri).inFile]);
290
290
  } catch {
@@ -254,9 +254,9 @@ export class RenderedDocsFeature {
254
254
 
255
255
  /* Whether names in `{@link}` and `@see` can be followed, which takes a server that finds symbols by name. */
256
256
  get canFollow(): boolean {
257
- const { project, uri } = this.language;
257
+ const { uri } = this.language;
258
258
 
259
- return project.service.supports('workspace/symbol', uri);
259
+ return this.language.service.supports('workspace/symbol', uri);
260
260
  }
261
261
 
262
262
  /* Goes to what a link in a comment names. */
@@ -21,14 +21,14 @@ export function selectionChainOf(selection: SelectionRange): EditorRange[] {
21
21
  */
22
22
  export class SelectionRangesFeature {
23
23
  constructor(language: EditorLanguage) {
24
- const { editor, project, uri } = language;
24
+ const { editor, uri } = language;
25
25
 
26
26
  editor.setSelectionRanges(async (positions) => {
27
- if (!project.service.supports(METHOD, uri)) {
27
+ if (!language.service.supports(METHOD, uri)) {
28
28
  return null;
29
29
  }
30
30
 
31
- const chains = await project.service.selectionRanges(uri, positions);
31
+ const chains = await language.service.selectionRanges(uri, positions);
32
32
 
33
33
  return chains === null || chains.length !== positions.length ? null : chains.map(selectionChainOf);
34
34
  });
package/src/signature.ts CHANGED
@@ -111,8 +111,8 @@ export class SignatureFeature {
111
111
  }
112
112
 
113
113
  private options(): { triggers: readonly string[]; retriggers: readonly string[] } | null {
114
- const { project, uri } = this.language;
115
- const options = project.service.providerOptions(METHOD, uri);
114
+ const { uri } = this.language;
115
+ const options = this.language.service.providerOptions(METHOD, uri);
116
116
 
117
117
  return options === undefined ? null : { triggers: options.triggerCharacters ?? [], retriggers: options.retriggerCharacters ?? [] };
118
118
  }
@@ -147,9 +147,9 @@ export class SignatureFeature {
147
147
  }
148
148
 
149
149
  private ask(context: Omit<SignatureHelpContext, 'activeSignatureHelp'>): void {
150
- const { project, uri } = this.language;
150
+ const { uri } = this.language;
151
151
 
152
- if (!project.service.supports(METHOD, uri)) {
152
+ if (!this.language.service.supports(METHOD, uri)) {
153
153
  return;
154
154
  }
155
155
 
@@ -160,7 +160,7 @@ export class SignatureFeature {
160
160
  }
161
161
 
162
162
  private async fetch(context: SignatureHelpContext, request: number): Promise<void> {
163
- const { editor, project, uri } = this.language;
163
+ const { editor, uri } = this.language;
164
164
  const caret = editor.getCaret();
165
165
  this.controller?.abort();
166
166
  const controller = new AbortController();
@@ -168,7 +168,7 @@ export class SignatureFeature {
168
168
  let help: SignatureHelp | null;
169
169
 
170
170
  try {
171
- help = await project.service.signatureHelp(uri, caret, context, { signal: controller.signal });
171
+ help = await this.language.service.signatureHelp(uri, caret, context, { signal: controller.signal });
172
172
  } catch {
173
173
  return;
174
174
  }
@@ -5,7 +5,7 @@ import { acquireSnippetTransforms, type SnippetTransformLease } from './snippet-
5
5
  import { realTimers, type Timers } from './timers.ts';
6
6
  import { KEY_PRIORITY } from './key-priority.ts';
7
7
  import { MAX_TRANSFORM_CHARACTERS, type SnippetTransformJob } from './snippet-transform-protocol.ts';
8
- import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
8
+ import type { EditorApplyEditsOptions, EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
9
9
  import { comparePositions, endOfInsertion, rangeHolds, sameRange, shiftPosition } from './diagnostics-model.ts';
10
10
  import type { EditorLanguage } from './editor-language.ts';
11
11
  import { textBeforeOf } from './line-text.ts';
@@ -367,7 +367,12 @@ export class SnippetFeature {
367
367
  }
368
368
 
369
369
  /* Imports can touch a hull's edge; their affinity must differ from edits inside its active field. */
370
- applyEdits(changes: readonly EditorContentChange[], token: number, extras: readonly EditorContentChange[] = []): boolean {
370
+ applyEdits(
371
+ changes: readonly EditorContentChange[],
372
+ token: number,
373
+ extras: readonly EditorContentChange[] = [],
374
+ options?: EditorApplyEditsOptions
375
+ ): boolean {
371
376
  const frame = this.frames.at(-1);
372
377
 
373
378
  const fits =
@@ -385,14 +390,14 @@ export class SnippetFeature {
385
390
  if (token !== this.generation || !fits) {
386
391
  this.end();
387
392
 
388
- return this.language.editor.applyEdits(changes);
393
+ return this.language.editor.applyEdits(changes, options);
389
394
  }
390
395
 
391
396
  this.applying = true;
392
397
  this.extras = extras;
393
398
 
394
399
  try {
395
- return this.language.editor.applyEdits(changes);
400
+ return this.language.editor.applyEdits(changes, options);
396
401
  } finally {
397
402
  this.applying = false;
398
403
  this.extras = [];
@@ -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