@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
package/dist/pick.js CHANGED
@@ -10,8 +10,14 @@ export class PickFeature {
10
10
  language;
11
11
  timers;
12
12
  spec = null;
13
+ /* The rows as the spec gave them, or as `remove` left them; `ids` are those a query lets through. */
14
+ groups = [];
13
15
  ids = [];
14
16
  active = '';
17
+ query = '';
18
+ /* The rows chosen as a run or one by one, in list order, and the row a run of them grows from. */
19
+ chosen = [];
20
+ rangeAnchor = null;
15
21
  previewTimer;
16
22
  controller = null;
17
23
  /* The rows that said why they cannot be taken, by id. */
@@ -42,6 +48,10 @@ export class PickFeature {
42
48
  }
43
49
  this.spec = spec;
44
50
  this.refusals = new Map();
51
+ this.groups = spec.groups;
52
+ this.query = '';
53
+ this.chosen = [];
54
+ this.rangeAnchor = null;
45
55
  this.ids = ids;
46
56
  this.active = spec.active !== undefined && ids.includes(spec.active) ? spec.active : ids[0];
47
57
  this.publish(null);
@@ -59,16 +69,40 @@ export class PickFeature {
59
69
  pick: null
60
70
  });
61
71
  }
62
- move(delta) {
72
+ /* Moves the active row; with `extend`, in a list of several choices, the rows between where the move began and the new row are chosen. */
73
+ move(delta, extend = false) {
74
+ if (this.ids.length === 0) {
75
+ return;
76
+ }
63
77
  const index = this.ids.indexOf(this.active);
64
- this.activate(this.ids[Math.max(0, Math.min(this.ids.length - 1, index + delta))]);
78
+ const id = this.ids[Math.max(0, Math.min(this.ids.length - 1, index + delta))];
79
+ if (extend && this.spec?.multiple === true) {
80
+ this.extendTo(id);
81
+ return;
82
+ }
83
+ this.settle(id);
84
+ }
85
+ /* Makes `id` the only row the list is on. */
86
+ settle(id) {
87
+ const hadChosen = this.chosen.length > 0;
88
+ this.chosen = [];
89
+ this.rangeAnchor = null;
90
+ this.activate(id, hadChosen);
65
91
  }
66
- /* Takes a row, by a click or Enter. */
67
- choose(id) {
92
+ /* Takes a row, by a click or Enter. In a list of several choices a press with Shift chooses the run up to the row, and one with Mod adds or takes away the row. */
93
+ choose(id, modifiers = {}) {
68
94
  const spec = this.spec;
69
95
  if (spec === null || !this.ids.includes(id) || this.attempting) {
70
96
  return;
71
97
  }
98
+ if (spec.multiple === true && modifiers.range === true) {
99
+ this.extendTo(id);
100
+ return;
101
+ }
102
+ if (spec.multiple === true && modifiers.toggle === true) {
103
+ this.toggle(id);
104
+ return;
105
+ }
72
106
  if (spec.attempt === undefined) {
73
107
  this.close();
74
108
  spec.accept(id);
@@ -89,25 +123,99 @@ export class PickFeature {
89
123
  this.publish(null);
90
124
  });
91
125
  }
92
- activate(id) {
93
- if (id === this.active) {
126
+ activate(id, force = false) {
127
+ const moved = id !== this.active;
128
+ this.active = id;
129
+ if (moved) {
130
+ this.controller?.abort();
131
+ this.schedulePreview();
132
+ }
133
+ if (moved || force) {
134
+ this.publish(null);
135
+ }
136
+ }
137
+ extendTo(id) {
138
+ const anchor = this.rangeAnchor ?? this.active;
139
+ const first = this.ids.indexOf(anchor);
140
+ const last = this.ids.indexOf(id);
141
+ const run = this.ids.slice(Math.min(first, last), Math.max(first, last) + 1);
142
+ this.rangeAnchor = anchor;
143
+ this.chosen = run.length > 1 ? run : [];
144
+ this.activate(id, true);
145
+ }
146
+ toggle(id) {
147
+ const held = this.chosen.length > 0 ? this.chosen : [this.active];
148
+ const next = held.includes(id) ? held.filter(other => other !== id) : [...held, id];
149
+ this.chosen = this.ids.filter(other => next.includes(other));
150
+ this.rangeAnchor = id;
151
+ this.activate(id, true);
152
+ }
153
+ /* Enter: the chosen rows, or the active one when none is chosen. */
154
+ takeActive() {
155
+ const spec = this.spec;
156
+ if (spec?.acceptMany === undefined) {
157
+ this.choose(this.active);
94
158
  return;
95
159
  }
96
- this.active = id;
160
+ const ids = this.chosen.length > 0 ? this.chosen : [this.active];
161
+ if (this.active !== '' && !this.attempting) {
162
+ this.close();
163
+ spec.acceptMany(ids);
164
+ }
165
+ }
166
+ visibleGroups() {
167
+ const query = this.query.toLowerCase();
168
+ if (query === '') {
169
+ return this.groups;
170
+ }
171
+ return this.groups.map(group => ({
172
+ ...group,
173
+ rows: group.rows.filter(row => (row.search ?? row.label).toLowerCase().includes(query))
174
+ })).filter(group => group.rows.length > 0);
175
+ }
176
+ /* The query changed: the rows it lets through, with the first of them active. */
177
+ requery(query) {
178
+ this.query = query;
179
+ this.ids = this.visibleGroups().flatMap(group => group.rows.map(row => row.id));
180
+ this.chosen = [];
181
+ this.rangeAnchor = null;
182
+ this.active = this.ids[0] ?? '';
183
+ this.controller?.abort();
184
+ this.publish(null);
185
+ this.schedulePreview();
186
+ }
187
+ /* Takes the chosen rows, or the active one, out of the list; the list closes when no row is left. */
188
+ removeChosen() {
189
+ const spec = this.spec;
190
+ if (spec?.remove === undefined || this.active === '') {
191
+ return;
192
+ }
193
+ const removed = this.chosen.length > 0 ? this.chosen : [this.active];
194
+ const index = this.ids.indexOf(removed[0]);
195
+ this.groups = spec.remove(removed);
196
+ if (this.groups.every(group => group.rows.length === 0)) {
197
+ this.close();
198
+ return;
199
+ }
200
+ this.ids = this.visibleGroups().flatMap(group => group.rows.map(row => row.id));
201
+ this.chosen = [];
202
+ this.rangeAnchor = null;
203
+ this.active = this.ids[Math.min(index, this.ids.length - 1)] ?? '';
97
204
  this.controller?.abort();
98
205
  this.publish(null);
99
206
  this.schedulePreview();
100
207
  }
101
208
  key(event) {
102
- if (this.spec === null || event.metaKey || event.ctrlKey || event.altKey) {
209
+ const spec = this.spec;
210
+ if (spec === null || event.metaKey || event.ctrlKey || event.altKey) {
103
211
  return false;
104
212
  }
105
213
  switch (event.key) {
106
214
  case 'ArrowDown':
107
- this.move(1);
215
+ this.move(1, event.shiftKey);
108
216
  return true;
109
217
  case 'ArrowUp':
110
- this.move(-1);
218
+ this.move(-1, event.shiftKey);
111
219
  return true;
112
220
  case 'PageDown':
113
221
  this.move(8);
@@ -116,17 +224,26 @@ export class PickFeature {
116
224
  this.move(-8);
117
225
  return true;
118
226
  case 'Home':
119
- this.activate(this.ids[0]);
120
- return true;
121
227
  case 'End':
122
- this.activate(this.ids[this.ids.length - 1]);
228
+ if (this.ids.length > 0) {
229
+ this.settle(event.key === 'Home' ? this.ids[0] : this.ids[this.ids.length - 1]);
230
+ }
123
231
  return true;
124
232
  case 'Enter':
125
- this.choose(this.active);
233
+ this.takeActive();
126
234
  return true;
235
+ case 'Delete':
236
+ this.removeChosen();
237
+ return spec.remove !== undefined;
238
+ case 'Backspace':
239
+ if (this.query !== '') {
240
+ this.requery(this.query.slice(0, -1));
241
+ return true;
242
+ }
243
+ this.removeChosen();
244
+ return spec.remove !== undefined || spec.filter === true;
127
245
  case 'Tab':
128
246
  {
129
- const spec = this.spec;
130
247
  if (spec.onTab !== undefined) {
131
248
  this.close();
132
249
  spec.onTab(event.shiftKey);
@@ -139,13 +256,17 @@ export class PickFeature {
139
256
  this.close();
140
257
  return true;
141
258
  default:
259
+ if (spec.filter === true && event.key.length === 1) {
260
+ this.requery(this.query + event.key);
261
+ return true;
262
+ }
142
263
  return false;
143
264
  }
144
265
  }
145
266
  schedulePreview() {
146
267
  const spec = this.spec;
147
268
  this.timers.clear(this.previewTimer);
148
- if (spec?.preview === undefined) {
269
+ if (spec?.preview === undefined || this.active === '') {
149
270
  return;
150
271
  }
151
272
  const id = this.active;
@@ -174,10 +295,12 @@ export class PickFeature {
174
295
  this.language.popups.setState({
175
296
  pick: {
176
297
  anchor: spec.anchor,
177
- groups: spec.groups,
298
+ groups: this.visibleGroups(),
178
299
  active: this.active,
179
300
  preview: shown,
180
- title: spec.title ?? null
301
+ title: spec.title ?? null,
302
+ query: spec.filter === true ? this.query : null,
303
+ chosen: this.chosen
181
304
  }
182
305
  });
183
306
  }
package/dist/popups.d.ts CHANGED
@@ -54,6 +54,7 @@ export interface PickRow {
54
54
  readonly label: string;
55
55
  readonly detail: string;
56
56
  readonly path?: boolean;
57
+ readonly search?: string;
57
58
  }
58
59
  export interface PickGroup {
59
60
  readonly title: string | null;
@@ -64,6 +65,7 @@ export interface PickPreview {
64
65
  readonly added: readonly string[];
65
66
  readonly note: string | null;
66
67
  readonly refusal?: string;
68
+ readonly kind?: 'diff' | 'text';
67
69
  }
68
70
  export interface PickView {
69
71
  readonly anchor: EditorPosition;
@@ -71,6 +73,8 @@ export interface PickView {
71
73
  readonly active: string;
72
74
  readonly preview: PickPreview | null;
73
75
  readonly title: string | null;
76
+ readonly query: string | null;
77
+ readonly chosen: readonly string[];
74
78
  }
75
79
  export interface RenameFileView {
76
80
  readonly uri: string;
@@ -134,6 +138,10 @@ export interface AuthorsView {
134
138
  readonly authorship: CodeAuthorship;
135
139
  readonly openCommit: (() => void) | null;
136
140
  }
141
+ export interface WrapView {
142
+ readonly anchor: EditorPosition;
143
+ readonly error: string | null;
144
+ }
137
145
  export interface PopupState {
138
146
  readonly hover: HoverView | null;
139
147
  readonly completion: CompletionView | null;
@@ -145,6 +153,7 @@ export interface PopupState {
145
153
  readonly symbols: SymbolsView | null;
146
154
  readonly menu: MenuView | null;
147
155
  readonly authors: AuthorsView | null;
156
+ readonly wrap: WrapView | null;
148
157
  }
149
158
  export type PopupStore = StoreApi<PopupState>;
150
159
  export declare function createPopupStore(): PopupStore;
package/dist/popups.js CHANGED
@@ -10,6 +10,7 @@ export function createPopupStore() {
10
10
  callHierarchy: null,
11
11
  symbols: null,
12
12
  menu: null,
13
- authors: null
13
+ authors: null,
14
+ wrap: null
14
15
  }));
15
16
  }
@@ -1,6 +1,8 @@
1
1
  import { type i18n } from 'i18next';
2
2
  import { type ApplyWorkspaceEditResult, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
3
3
  import type { Editor, EditorPosition } from '@adecore/editor';
4
+ import { ClipboardHistory } from './clipboard-history.ts';
5
+ import { EditLocations } from './edit-locations.ts';
4
6
  import { NavigationHistory, type Place } from './navigation-history.ts';
5
7
  import { ProjectProblems } from './project-problems.ts';
6
8
  import type { LanguageHost } from './host-types.ts';
@@ -10,6 +12,9 @@ export interface LanguageDocumentHandle {
10
12
  readonly ready: Promise<void>;
11
13
  release(): void;
12
14
  }
15
+ export interface AcquireOptions {
16
+ readonly serve?: boolean;
17
+ }
13
18
  export type FileOperationTarget = string | {
14
19
  readonly path: string;
15
20
  readonly directory?: boolean;
@@ -21,6 +26,8 @@ export declare class ProjectLanguage {
21
26
  readonly folder: string;
22
27
  readonly problems: ProjectProblems;
23
28
  readonly history: NavigationHistory;
29
+ readonly edits: EditLocations;
30
+ readonly clipboard: ClipboardHistory;
24
31
  private readonly holders;
25
32
  private readonly files;
26
33
  panelBodyHeight: number;
@@ -28,6 +35,8 @@ export declare class ProjectLanguage {
28
35
  private pendingCaret;
29
36
  private lastPlace;
30
37
  private disposed;
38
+ private readonly subscriptions;
39
+ private readonly workspaceResults;
31
40
  constructor(service: LanguageService, host?: LanguageHost);
32
41
  onPanelBodyChange(listener: (height: number) => void): () => void;
33
42
  resizePanelBody(height: number): void;
@@ -38,11 +47,19 @@ export declare class ProjectLanguage {
38
47
  willDeleteFiles(targets: readonly FileOperationTarget[]): Promise<WorkspaceEdit | null>;
39
48
  didDeleteFiles(targets: readonly FileOperationTarget[]): Promise<void>;
40
49
  readText(uri: string): Promise<string | null>;
50
+ pullWorkspaceProblems(uri: string, options?: {
51
+ readonly signal?: AbortSignal;
52
+ }): Promise<boolean>;
53
+ private showDocument;
41
54
  openPlace(place: Place): void;
42
55
  noteCaret(place: Place): void;
43
56
  jumpTo(place: Place): void;
44
57
  takeCaret(uri: string, line: number): EditorPosition | null;
45
- acquire(uri: string, languageId: string, editor: Editor): LanguageDocumentHandle;
58
+ openEditors(): readonly {
59
+ uri: string;
60
+ editor: Editor;
61
+ }[];
62
+ acquire(uri: string, languageId: string, editor: Editor, options?: AcquireOptions): LanguageDocumentHandle;
46
63
  private release;
47
64
  dispose(): void;
48
65
  }
@@ -1,11 +1,14 @@
1
1
  import { disposeSnippetTransforms } from './snippet-transform-queue.js';
2
2
  import i18next from 'i18next';
3
- import { fileUriToPath, pathToFileUri } from '@adecore/lsp';
3
+ import { fileUriToPath, pathToFileUri, unservedLanguageService } from '@adecore/lsp';
4
+ import { ClipboardHistory } from './clipboard-history.js';
5
+ import { EditLocations } from './edit-locations.js';
4
6
  import { NavigationHistory } from './navigation-history.js';
5
7
  import { ProjectProblems, serviceProblems } from './project-problems.js';
6
8
  import { applyWorkspaceEdit } from './workspace-edit.js';
7
9
  import { relativeTo } from './paths.js';
8
10
  import { PANEL_BODY_HEIGHT } from './panel-size.js';
11
+ const UNSERVED = unservedLanguageService();
9
12
  /* Sends one editor's edits to the document, holding them until the document is open. */
10
13
  class DocumentSync {
11
14
  ready;
@@ -86,6 +89,8 @@ export class ProjectLanguage {
86
89
  folder;
87
90
  problems;
88
91
  history = new NavigationHistory();
92
+ edits;
93
+ clipboard;
89
94
  holders = new Map();
90
95
  files;
91
96
  /* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
@@ -94,14 +99,22 @@ export class ProjectLanguage {
94
99
  pendingCaret = null;
95
100
  lastPlace = null;
96
101
  disposed = false;
102
+ subscriptions;
103
+ /* The result id the server gave each document in its last workspace report, which the next asks with. */
104
+ workspaceResults = new Map();
97
105
  constructor(service, host = {}) {
98
106
  this.service = service;
99
107
  this.host = host;
100
108
  this.i18n = host.i18n ?? i18next;
101
109
  this.folder = host.folder ?? '';
102
110
  this.files = host.files ?? null;
111
+ this.edits = new EditLocations(host.editHistoryLimit);
112
+ this.clipboard = host.clipboard ?? new ClipboardHistory();
103
113
  this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
104
114
  this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, uri => this.pathOfLocation(uri) ?? uri));
115
+ const shows = service.onShowDocument?.(params => this.showDocument(params));
116
+ const messages = host.onServerMessage === undefined ? undefined : service.onServerMessage?.(message => host.onServerMessage(message));
117
+ this.subscriptions = [shows, messages].flatMap(subscription => subscription === undefined ? [] : [subscription]);
105
118
  }
106
119
  /* Open panels of other editors of the project follow a resize. */
107
120
  onPanelBodyChange(listener) {
@@ -153,16 +166,86 @@ export class ProjectLanguage {
153
166
  }
154
167
  /* Tells the servers that files were deleted, once they are. */
155
168
  didDeleteFiles(targets) {
156
- return this.service.didDeleteFiles?.(filesOf(targets)) ?? Promise.resolve();
169
+ const files = filesOf(targets);
170
+ this.edits.forget(uri => files.some(file => file.directory ? uri.startsWith(`${file.uri}/`) : uri === file.uri));
171
+ return this.service.didDeleteFiles?.(files) ?? Promise.resolve();
157
172
  }
158
- /* 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. */
173
+ /*
174
+ * 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
175
+ * text file. A document of a scheme a server owns, such as a decompiled class, is read from that server.
176
+ */
159
177
  async readText(uri) {
160
- const open = this.holders.get(uri)?.sync.text;
178
+ const open = this.holders.get(uri)?.editors[0]?.getText();
161
179
  if (open !== undefined) {
162
180
  return open;
163
181
  }
164
182
  const path = fileUriToPath(uri);
165
- return path === null || this.files === null ? null : (await this.files.read(path))?.text ?? null;
183
+ if (path === null) {
184
+ return (await this.service.textDocumentContent?.(uri).catch(() => null)) ?? null;
185
+ }
186
+ return this.files === null ? null : (await this.files.read(path))?.text ?? null;
187
+ }
188
+ /*
189
+ * Asks the server of `uri` for the problems of every file it knows and lists them in `problems`, for a server that
190
+ * offers workspace diagnostics. A file the server reports as unchanged keeps what it had. False when no server offers them.
191
+ */
192
+ async pullWorkspaceProblems(uri, options = {}) {
193
+ const {
194
+ service
195
+ } = this;
196
+ if (service.workspaceDiagnostics === undefined || !service.supports('workspace/diagnostic', uri)) {
197
+ return false;
198
+ }
199
+ const previous = [...this.workspaceResults].map(([key, value]) => ({
200
+ uri: key,
201
+ value
202
+ }));
203
+ const report = await service.workspaceDiagnostics(uri, previous, options);
204
+ const server = this.host.serverNames?.(uri)[0] ?? 'workspace';
205
+ for (const item of report.items) {
206
+ if (item.resultId !== undefined) {
207
+ this.workspaceResults.set(item.uri, item.resultId);
208
+ }
209
+ if (item.kind === 'full') {
210
+ this.problems.report(this.pathOfLocation(item.uri) ?? item.uri, server, item.items);
211
+ }
212
+ }
213
+ return true;
214
+ }
215
+ /* Shows what a server asks for: a web address outside the editor, anything else as a place of the host. */
216
+ async showDocument(params) {
217
+ const web = /^(https?|mailto):/i.test(params.uri);
218
+ if (params.external === true || web) {
219
+ if (this.host.openExternal !== undefined) {
220
+ this.host.openExternal(params.uri);
221
+ return {
222
+ success: true
223
+ };
224
+ }
225
+ if (typeof window === 'undefined') {
226
+ return {
227
+ success: false
228
+ };
229
+ }
230
+ return {
231
+ success: window.open(params.uri, '_blank', 'noopener,noreferrer') !== null
232
+ };
233
+ }
234
+ if (this.host.openPlace === undefined) {
235
+ return {
236
+ success: false
237
+ };
238
+ }
239
+ this.openPlace({
240
+ uri: params.uri,
241
+ position: params.selection?.start ?? {
242
+ line: 0,
243
+ character: 0
244
+ }
245
+ });
246
+ return {
247
+ success: true
248
+ };
166
249
  }
167
250
  /*
168
251
  * Opens the file of a place on its line. A file opens on a line only, so the column is kept here until the editor
@@ -192,27 +275,40 @@ export class ProjectLanguage {
192
275
  this.pendingCaret = null;
193
276
  return pending.position;
194
277
  }
278
+ /* The editors that hold a document, one per uri in the order the documents were first opened. */
279
+ openEditors() {
280
+ return [...this.holders].flatMap(([uri, holder]) => holder.editors[0] === undefined ? [] : [{
281
+ uri,
282
+ editor: holder.editors[0]
283
+ }]);
284
+ }
195
285
  /* Opens `uri` for the editor, or joins the editors that already hold it. */
196
- acquire(uri, languageId, editor) {
286
+ acquire(uri, languageId, editor, options = {}) {
197
287
  if (this.disposed) {
198
288
  throw new Error('The project language is disposed');
199
289
  }
290
+ const serve = options.serve ?? true;
200
291
  let holder = this.holders.get(uri);
201
292
  if (holder === undefined) {
202
293
  holder = {
203
294
  editors: [],
204
- sync: new DocumentSync(this.service, uri, languageId, editor),
295
+ served: [],
296
+ sync: null,
205
297
  languageId
206
298
  };
207
299
  this.holders.set(uri, holder);
208
300
  }
209
301
  holder.editors.push(editor);
302
+ if (serve) {
303
+ holder.served.push(editor);
304
+ holder.sync ??= new DocumentSync(this.service, uri, languageId, editor);
305
+ }
210
306
  let released = false;
211
307
  return {
212
308
  uri,
213
- service: this.service,
309
+ service: serve ? this.service : UNSERVED,
214
310
  get ready() {
215
- return holder.sync.ready;
311
+ return serve ? holder.sync?.ready ?? Promise.resolve() : Promise.resolve();
216
312
  },
217
313
  release: () => {
218
314
  if (!released) {
@@ -228,20 +324,29 @@ export class ProjectLanguage {
228
324
  return;
229
325
  }
230
326
  holder.editors.splice(holder.editors.indexOf(editor), 1);
231
- const next = holder.editors[0];
232
- if (next === undefined) {
233
- holder.sync.dispose();
327
+ if (holder.served.includes(editor)) {
328
+ holder.served.splice(holder.served.indexOf(editor), 1);
329
+ }
330
+ const {
331
+ sync
332
+ } = holder;
333
+ if (holder.editors.length === 0) {
234
334
  this.holders.delete(uri);
235
- void holder.sync.ready.catch(() => undefined).then(() => {
236
- if (this.holders.has(uri)) {
335
+ }
336
+ if (sync === null || sync.editor !== editor) {
337
+ return;
338
+ }
339
+ sync.dispose();
340
+ const next = holder.served[0];
341
+ // The editor that sent the changes is gone; the next served one takes over and opens the document with its own text.
342
+ holder.sync = next === undefined ? null : new DocumentSync(this.service, uri, holder.languageId, next);
343
+ if (holder.sync === null) {
344
+ void sync.ready.catch(() => undefined).then(() => {
345
+ if (this.holders.get(uri)?.sync) {
237
346
  return undefined;
238
347
  }
239
348
  return this.service.closeDocument(uri);
240
349
  }).catch(() => undefined);
241
- } else if (holder.sync.editor === editor) {
242
- // The editor that sent the changes is gone; the next one takes over and opens the document with its own text.
243
- holder.sync.dispose();
244
- holder.sync = new DocumentSync(this.service, uri, holder.languageId, next);
245
350
  }
246
351
  }
247
352
  dispose() {
@@ -249,8 +354,14 @@ export class ProjectLanguage {
249
354
  return;
250
355
  }
251
356
  this.disposed = true;
357
+ for (const subscription of this.subscriptions) {
358
+ subscription.dispose();
359
+ }
252
360
  disposeSnippetTransforms(this);
253
361
  for (const [uri, holder] of this.holders) {
362
+ if (holder.sync === null) {
363
+ continue;
364
+ }
254
365
  holder.sync.dispose();
255
366
  void holder.sync.ready.catch(() => undefined).then(() => this.service.closeDocument(uri)).catch(() => undefined);
256
367
  }
package/dist/rename.js CHANGED
@@ -44,19 +44,18 @@ export class RenameFeature {
44
44
  async start() {
45
45
  const {
46
46
  editor,
47
- project,
48
47
  uri
49
48
  } = this.language;
50
- if (!project.service.supports(METHOD, uri)) {
49
+ if (!this.language.service.supports(METHOD, uri)) {
51
50
  this.tell('error', this.say('unavailable'));
52
51
  return;
53
52
  }
54
53
  this.cancel();
55
54
  const caret = editor.getCaret();
56
- const prepare = project.service.providerOptions(METHOD, uri)?.prepareProvider === true;
55
+ const prepare = this.language.service.providerOptions(METHOD, uri)?.prepareProvider === true;
57
56
  let answer;
58
57
  try {
59
- answer = prepare ? await project.service.prepareRename(uri, caret) : {
58
+ answer = prepare ? await this.language.service.prepareRename(uri, caret) : {
60
59
  defaultBehavior: true
61
60
  };
62
61
  } catch (error) {
@@ -110,7 +109,7 @@ export class RenameFeature {
110
109
  uri
111
110
  } = this.language;
112
111
  try {
113
- const edit = await project.service.rename(uri, session.target.range.start, next);
112
+ const edit = await this.language.service.rename(uri, session.target.range.start, next);
114
113
  if (this.session !== session) {
115
114
  return;
116
115
  }
@@ -204,14 +203,13 @@ export class RenameFeature {
204
203
  async light(session) {
205
204
  const {
206
205
  editor,
207
- project,
208
206
  uri
209
207
  } = this.language;
210
- if (!project.service.supports('textDocument/references', uri)) {
208
+ if (!this.language.service.supports('textDocument/references', uri)) {
211
209
  return null;
212
210
  }
213
211
  try {
214
- const locations = await project.service.references(uri, session.target.range.start, true);
212
+ const locations = await this.language.service.references(uri, session.target.range.start, true);
215
213
  if (this.session !== session || locations === null) {
216
214
  return null;
217
215
  }
@@ -282,14 +280,13 @@ export class RenameFeature {
282
280
  /* 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. */
283
281
  async kindOf(session, locations) {
284
282
  const {
285
- project,
286
283
  uri
287
284
  } = this.language;
288
- if (!project.service.supports('textDocument/documentSymbol', uri)) {
285
+ if (!this.language.service.supports('textDocument/documentSymbol', uri)) {
289
286
  return undefined;
290
287
  }
291
288
  try {
292
- const symbols = await project.service.documentSymbols(uri, {
289
+ const symbols = await this.language.service.documentSymbols(uri, {
293
290
  signal: session.names.signal,
294
291
  parallel: true
295
292
  });
@@ -192,10 +192,9 @@ export class RenderedDocsFeature {
192
192
  /* Whether names in `{@link}` and `@see` can be followed, which takes a server that finds symbols by name. */
193
193
  get canFollow() {
194
194
  const {
195
- project,
196
195
  uri
197
196
  } = this.language;
198
- return project.service.supports('workspace/symbol', uri);
197
+ return this.language.service.supports('workspace/symbol', uri);
199
198
  }
200
199
  /* Goes to what a link in a comment names. */
201
200
  follow(name, comment) {
@@ -15,14 +15,13 @@ export class SelectionRangesFeature {
15
15
  constructor(language) {
16
16
  const {
17
17
  editor,
18
- project,
19
18
  uri
20
19
  } = language;
21
20
  editor.setSelectionRanges(async positions => {
22
- if (!project.service.supports(METHOD, uri)) {
21
+ if (!language.service.supports(METHOD, uri)) {
23
22
  return null;
24
23
  }
25
- const chains = await project.service.selectionRanges(uri, positions);
24
+ const chains = await language.service.selectionRanges(uri, positions);
26
25
  return chains === null || chains.length !== positions.length ? null : chains.map(selectionChainOf);
27
26
  });
28
27
  language.onDispose(() => editor.setSelectionRanges(null));