@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.
- package/dist/CallHierarchyPanel.js +177 -181
- package/dist/EditorContextMenu.js +118 -105
- package/dist/LanguagePopups.js +7 -0
- package/dist/PickPopup.js +245 -104
- package/dist/WrapCard.d.ts +8 -0
- package/dist/WrapCard.js +172 -0
- package/dist/call-hierarchy-model.d.ts +4 -2
- package/dist/call-hierarchy-model.js +20 -0
- package/dist/call-hierarchy.d.ts +3 -0
- package/dist/call-hierarchy.js +54 -22
- package/dist/clipboard-history.d.ts +31 -0
- package/dist/clipboard-history.js +70 -0
- package/dist/code-actions.d.ts +3 -1
- package/dist/code-actions.js +50 -10
- package/dist/code-vision.js +2 -3
- package/dist/completion.d.ts +28 -0
- package/dist/completion.js +83 -27
- package/dist/context-menu-rows.d.ts +1 -1
- package/dist/context-menu-rows.js +4 -0
- package/dist/definition-link.js +15 -5
- package/dist/document-colors.d.ts +24 -0
- package/dist/document-colors.js +194 -0
- package/dist/document-links.d.ts +22 -0
- package/dist/document-links.js +133 -0
- package/dist/document-query.d.ts +15 -0
- package/dist/document-query.js +54 -0
- package/dist/edit-locations.d.ts +20 -0
- package/dist/edit-locations.js +136 -0
- package/dist/editor-language.d.ts +23 -1
- package/dist/editor-language.js +83 -2
- package/dist/emmet.d.ts +13 -0
- package/dist/emmet.js +62 -0
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +3 -5
- package/dist/highlights.js +2 -3
- package/dist/history.d.ts +7 -0
- package/dist/history.js +79 -1
- package/dist/host-types.d.ts +10 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +10 -0
- package/dist/inlay-hints.js +2 -2
- package/dist/inline-completion.d.ts +39 -0
- package/dist/inline-completion.js +221 -0
- package/dist/key-priority.d.ts +9 -5
- package/dist/key-priority.js +11 -6
- package/dist/linked-editing.d.ts +26 -0
- package/dist/linked-editing.js +215 -0
- package/dist/live-templates.d.ts +16 -0
- package/dist/live-templates.js +289 -0
- package/dist/locales/en.json +61 -2
- package/dist/locales/nl.json +61 -2
- package/dist/markup-context.d.ts +55 -0
- package/dist/markup-context.js +408 -0
- package/dist/markup.d.ts +39 -0
- package/dist/markup.js +465 -0
- package/dist/navigation.js +4 -6
- package/dist/paste-history.d.ts +7 -0
- package/dist/paste-history.js +104 -0
- package/dist/peek.js +3 -4
- package/dist/pick.d.ts +21 -2
- package/dist/pick.js +152 -19
- package/dist/popups.d.ts +10 -0
- package/dist/popups.js +2 -1
- package/dist/postfix.d.ts +21 -0
- package/dist/postfix.js +212 -0
- package/dist/project-language.d.ts +20 -1
- package/dist/project-language.js +132 -18
- package/dist/rename.js +8 -11
- package/dist/rendered-docs.js +1 -2
- package/dist/selection-ranges.js +2 -3
- package/dist/shortcut-keys.d.ts +24 -0
- package/dist/signature.js +3 -6
- package/dist/snippet-insert.d.ts +10 -0
- package/dist/snippet-insert.js +217 -0
- package/dist/snippet-session.d.ts +2 -2
- package/dist/snippet-session.js +3 -3
- package/dist/snippet.d.ts +7 -1
- package/dist/snippet.js +65 -8
- package/dist/surround-constructs.d.ts +10 -0
- package/dist/surround-constructs.js +100 -0
- package/dist/surround.d.ts +18 -0
- package/dist/surround.js +399 -0
- package/dist/symbol-picker.js +4 -6
- package/dist/symbols.js +22 -3
- package/dist/template-builtins.d.ts +3 -0
- package/dist/template-builtins.js +46 -0
- package/dist/template-context.d.ts +12 -0
- package/dist/template-context.js +56 -0
- package/dist/templates.d.ts +59 -0
- package/dist/templates.js +103 -0
- package/dist/testing.d.ts +23 -1
- package/dist/testing.js +114 -0
- package/package.json +7 -6
- package/src/CallHierarchyPanel.tsx +26 -11
- package/src/EditorContextMenu.tsx +7 -0
- package/src/LanguagePopups.tsx +4 -0
- package/src/PickPopup.tsx +47 -5
- package/src/WrapCard.tsx +64 -0
- package/src/call-hierarchy-model.ts +25 -3
- package/src/call-hierarchy.ts +62 -20
- package/src/clipboard-history.ts +102 -0
- package/src/code-actions.ts +48 -11
- package/src/code-vision.ts +3 -3
- package/src/completion.ts +123 -25
- package/src/context-menu-rows.ts +3 -2
- package/src/definition-link.ts +17 -5
- package/src/document-colors.ts +189 -0
- package/src/document-links.ts +129 -0
- package/src/document-query.ts +71 -0
- package/src/edit-locations.ts +162 -0
- package/src/editor-language.ts +86 -3
- package/src/emmet-module.d.ts +44 -0
- package/src/emmet.ts +73 -0
- package/src/folding.ts +3 -3
- package/src/format-on-type.ts +5 -5
- package/src/highlights.ts +3 -3
- package/src/history.ts +81 -1
- package/src/host-types.ts +17 -0
- package/src/index.ts +28 -3
- package/src/inlay-hints.ts +2 -2
- package/src/inline-completion.ts +221 -0
- package/src/key-priority.ts +11 -6
- package/src/linked-editing.ts +199 -0
- package/src/live-templates.ts +315 -0
- package/src/locales/en.json +61 -2
- package/src/locales/nl.json +61 -2
- package/src/markup-context.ts +387 -0
- package/src/markup.ts +377 -0
- package/src/navigation.ts +6 -6
- package/src/paste-history.ts +111 -0
- package/src/peek.ts +4 -4
- package/src/pick.ts +218 -20
- package/src/popups.ts +21 -1
- package/src/postfix.ts +224 -0
- package/src/project-language.ts +139 -19
- package/src/rename.ts +11 -11
- package/src/rendered-docs.ts +2 -2
- package/src/selection-ranges.ts +3 -3
- package/src/signature.ts +6 -6
- package/src/snippet-insert.ts +258 -0
- package/src/snippet-session.ts +9 -4
- package/src/snippet.ts +82 -7
- package/src/surround-constructs.ts +85 -0
- package/src/surround.ts +430 -0
- package/src/symbol-picker.ts +6 -6
- package/src/symbols.ts +30 -4
- package/src/template-builtins.ts +94 -0
- package/src/template-context.ts +65 -0
- package/src/templates.ts +197 -0
- package/src/testing.ts +126 -1
package/src/project-language.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { disposeSnippetTransforms } from './snippet-transform-queue.ts';
|
|
2
2
|
import i18next, { type i18n } from 'i18next';
|
|
3
|
-
import {
|
|
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 { TemplateRegistry } from './templates.ts';
|
|
18
|
+
import { EditLocations } from './edit-locations.ts';
|
|
5
19
|
import { NavigationHistory, type Place } from './navigation-history.ts';
|
|
6
20
|
import { ProjectProblems, serviceProblems } from './project-problems.ts';
|
|
7
21
|
import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
|
|
@@ -16,6 +30,8 @@ export interface LanguageDocumentHandle {
|
|
|
16
30
|
release(): void;
|
|
17
31
|
}
|
|
18
32
|
|
|
33
|
+
const UNSERVED = unservedLanguageService();
|
|
34
|
+
|
|
19
35
|
/* Sends one editor's edits to the document, holding them until the document is open. */
|
|
20
36
|
class DocumentSync {
|
|
21
37
|
readonly ready: Promise<void>;
|
|
@@ -76,10 +92,18 @@ class DocumentSync {
|
|
|
76
92
|
|
|
77
93
|
interface Holder {
|
|
78
94
|
readonly editors: Editor[];
|
|
79
|
-
|
|
95
|
+
/* The editors a server serves, in the order they came; the first one sends the changes. */
|
|
96
|
+
readonly served: Editor[];
|
|
97
|
+
/* Null while no editor of the document is served, so nothing of it is open in the service. */
|
|
98
|
+
sync: DocumentSync | null;
|
|
80
99
|
languageId: string;
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
export interface AcquireOptions {
|
|
103
|
+
/* False registers the editor as open without telling the service, for a document no server serves; true by default. */
|
|
104
|
+
readonly serve?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
/* A file or folder for a file operation notification: a bare path is a file. */
|
|
84
108
|
export type FileOperationTarget = string | { readonly path: string; readonly directory?: boolean };
|
|
85
109
|
|
|
@@ -103,6 +127,9 @@ export class ProjectLanguage {
|
|
|
103
127
|
readonly folder: string;
|
|
104
128
|
readonly problems: ProjectProblems;
|
|
105
129
|
readonly history = new NavigationHistory();
|
|
130
|
+
readonly edits: EditLocations;
|
|
131
|
+
readonly clipboard: ClipboardHistory;
|
|
132
|
+
readonly templates: TemplateRegistry;
|
|
106
133
|
private readonly holders = new Map<string, Holder>();
|
|
107
134
|
private readonly files: ProjectFiles | null;
|
|
108
135
|
|
|
@@ -113,6 +140,9 @@ export class ProjectLanguage {
|
|
|
113
140
|
private pendingCaret: Place | null = null;
|
|
114
141
|
private lastPlace: Place | null = null;
|
|
115
142
|
private disposed = false;
|
|
143
|
+
private readonly subscriptions: readonly Disposable[];
|
|
144
|
+
/* The result id the server gave each document in its last workspace report, which the next asks with. */
|
|
145
|
+
private readonly workspaceResults = new Map<string, string>();
|
|
116
146
|
|
|
117
147
|
constructor(service: LanguageService, host: LanguageHost = {}) {
|
|
118
148
|
this.service = service;
|
|
@@ -120,8 +150,14 @@ export class ProjectLanguage {
|
|
|
120
150
|
this.i18n = host.i18n ?? i18next;
|
|
121
151
|
this.folder = host.folder ?? '';
|
|
122
152
|
this.files = host.files ?? null;
|
|
153
|
+
this.edits = new EditLocations(host.editHistoryLimit);
|
|
154
|
+
this.clipboard = host.clipboard ?? new ClipboardHistory();
|
|
155
|
+
this.templates = host.templates ?? new TemplateRegistry();
|
|
123
156
|
this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
|
|
124
157
|
this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
|
|
158
|
+
const shows = service.onShowDocument?.((params) => this.showDocument(params));
|
|
159
|
+
const messages = host.onServerMessage === undefined ? undefined : service.onServerMessage?.((message) => host.onServerMessage!(message));
|
|
160
|
+
this.subscriptions = [shows, messages].flatMap((subscription) => (subscription === undefined ? [] : [subscription]));
|
|
125
161
|
}
|
|
126
162
|
|
|
127
163
|
/* Open panels of other editors of the project follow a resize. */
|
|
@@ -182,20 +218,73 @@ export class ProjectLanguage {
|
|
|
182
218
|
|
|
183
219
|
/* Tells the servers that files were deleted, once they are. */
|
|
184
220
|
didDeleteFiles(targets: readonly FileOperationTarget[]): Promise<void> {
|
|
185
|
-
|
|
221
|
+
const files = filesOf(targets);
|
|
222
|
+
|
|
223
|
+
this.edits.forget((uri) => files.some((file) => (file.directory ? uri.startsWith(`${file.uri}/`) : uri === file.uri)));
|
|
224
|
+
|
|
225
|
+
return this.service.didDeleteFiles?.(files) ?? Promise.resolve();
|
|
186
226
|
}
|
|
187
227
|
|
|
188
|
-
/*
|
|
228
|
+
/*
|
|
229
|
+
* 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
|
|
230
|
+
* text file. A document of a scheme a server owns, such as a decompiled class, is read from that server.
|
|
231
|
+
*/
|
|
189
232
|
async readText(uri: string): Promise<string | null> {
|
|
190
|
-
const open = this.holders.get(uri)?.
|
|
233
|
+
const open = this.holders.get(uri)?.editors[0]?.getText();
|
|
191
234
|
|
|
192
235
|
if (open !== undefined) {
|
|
193
236
|
return open;
|
|
194
237
|
}
|
|
195
238
|
|
|
196
239
|
const path = fileUriToPath(uri);
|
|
240
|
+
if (path === null) {
|
|
241
|
+
return (await this.service.textDocumentContent?.(uri).catch(() => null)) ?? null;
|
|
242
|
+
}
|
|
197
243
|
|
|
198
|
-
return
|
|
244
|
+
return this.files === null ? null : ((await this.files.read(path))?.text ?? null);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
* Asks the server of `uri` for the problems of every file it knows and lists them in `problems`, for a server that
|
|
249
|
+
* offers workspace diagnostics. A file the server reports as unchanged keeps what it had. False when no server offers them.
|
|
250
|
+
*/
|
|
251
|
+
async pullWorkspaceProblems(uri: string, options: { readonly signal?: AbortSignal } = {}): Promise<boolean> {
|
|
252
|
+
const { service } = this;
|
|
253
|
+
if (service.workspaceDiagnostics === undefined || !service.supports('workspace/diagnostic', uri)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const previous = [...this.workspaceResults].map(([key, value]) => ({ uri: key, value }));
|
|
257
|
+
const report = await service.workspaceDiagnostics(uri, previous, options);
|
|
258
|
+
const server = this.host.serverNames?.(uri)[0] ?? 'workspace';
|
|
259
|
+
for (const item of report.items) {
|
|
260
|
+
if (item.resultId !== undefined) {
|
|
261
|
+
this.workspaceResults.set(item.uri, item.resultId);
|
|
262
|
+
}
|
|
263
|
+
if (item.kind === 'full') {
|
|
264
|
+
this.problems.report(this.pathOfLocation(item.uri) ?? item.uri, server, item.items);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Shows what a server asks for: a web address outside the editor, anything else as a place of the host. */
|
|
271
|
+
private async showDocument(params: ShowDocumentParams): Promise<ShowDocumentResult> {
|
|
272
|
+
const web = /^(https?|mailto):/i.test(params.uri);
|
|
273
|
+
if (params.external === true || web) {
|
|
274
|
+
if (this.host.openExternal !== undefined) {
|
|
275
|
+
this.host.openExternal(params.uri);
|
|
276
|
+
return { success: true };
|
|
277
|
+
}
|
|
278
|
+
if (typeof window === 'undefined') {
|
|
279
|
+
return { success: false };
|
|
280
|
+
}
|
|
281
|
+
return { success: window.open(params.uri, '_blank', 'noopener,noreferrer') !== null };
|
|
282
|
+
}
|
|
283
|
+
if (this.host.openPlace === undefined) {
|
|
284
|
+
return { success: false };
|
|
285
|
+
}
|
|
286
|
+
this.openPlace({ uri: params.uri, position: params.selection?.start ?? { line: 0, character: 0 } });
|
|
287
|
+
return { success: true };
|
|
199
288
|
}
|
|
200
289
|
|
|
201
290
|
/*
|
|
@@ -234,27 +323,39 @@ export class ProjectLanguage {
|
|
|
234
323
|
return pending.position;
|
|
235
324
|
}
|
|
236
325
|
|
|
326
|
+
/* The editors that hold a document, one per uri in the order the documents were first opened. */
|
|
327
|
+
openEditors(): readonly { uri: string; editor: Editor }[] {
|
|
328
|
+
return [...this.holders].flatMap(([uri, holder]) => (holder.editors[0] === undefined ? [] : [{ uri, editor: holder.editors[0] }]));
|
|
329
|
+
}
|
|
330
|
+
|
|
237
331
|
/* Opens `uri` for the editor, or joins the editors that already hold it. */
|
|
238
|
-
acquire(uri: string, languageId: string, editor: Editor): LanguageDocumentHandle {
|
|
332
|
+
acquire(uri: string, languageId: string, editor: Editor, options: AcquireOptions = {}): LanguageDocumentHandle {
|
|
239
333
|
if (this.disposed) {
|
|
240
334
|
throw new Error('The project language is disposed');
|
|
241
335
|
}
|
|
242
336
|
|
|
337
|
+
const serve = options.serve ?? true;
|
|
243
338
|
let holder = this.holders.get(uri);
|
|
244
339
|
|
|
245
340
|
if (holder === undefined) {
|
|
246
|
-
holder = { editors: [],
|
|
341
|
+
holder = { editors: [], served: [], sync: null, languageId };
|
|
247
342
|
this.holders.set(uri, holder);
|
|
248
343
|
}
|
|
249
344
|
|
|
250
345
|
holder.editors.push(editor);
|
|
346
|
+
|
|
347
|
+
if (serve) {
|
|
348
|
+
holder.served.push(editor);
|
|
349
|
+
holder.sync ??= new DocumentSync(this.service, uri, languageId, editor);
|
|
350
|
+
}
|
|
351
|
+
|
|
251
352
|
let released = false;
|
|
252
353
|
|
|
253
354
|
return {
|
|
254
355
|
uri,
|
|
255
|
-
service: this.service,
|
|
356
|
+
service: serve ? this.service : UNSERVED,
|
|
256
357
|
get ready() {
|
|
257
|
-
return holder.sync.
|
|
358
|
+
return serve ? (holder.sync?.ready ?? Promise.resolve()) : Promise.resolve();
|
|
258
359
|
},
|
|
259
360
|
release: () => {
|
|
260
361
|
if (!released) {
|
|
@@ -273,26 +374,38 @@ export class ProjectLanguage {
|
|
|
273
374
|
}
|
|
274
375
|
|
|
275
376
|
holder.editors.splice(holder.editors.indexOf(editor), 1);
|
|
276
|
-
const next = holder.editors[0];
|
|
277
377
|
|
|
278
|
-
if (
|
|
279
|
-
holder.
|
|
378
|
+
if (holder.served.includes(editor)) {
|
|
379
|
+
holder.served.splice(holder.served.indexOf(editor), 1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { sync } = holder;
|
|
383
|
+
|
|
384
|
+
if (holder.editors.length === 0) {
|
|
280
385
|
this.holders.delete(uri);
|
|
386
|
+
}
|
|
281
387
|
|
|
282
|
-
|
|
388
|
+
if (sync === null || sync.editor !== editor) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
sync.dispose();
|
|
393
|
+
const next = holder.served[0];
|
|
394
|
+
|
|
395
|
+
// The editor that sent the changes is gone; the next served one takes over and opens the document with its own text.
|
|
396
|
+
holder.sync = next === undefined ? null : new DocumentSync(this.service, uri, holder.languageId, next);
|
|
397
|
+
|
|
398
|
+
if (holder.sync === null) {
|
|
399
|
+
void sync.ready
|
|
283
400
|
.catch(() => undefined)
|
|
284
401
|
.then(() => {
|
|
285
|
-
if (this.holders.
|
|
402
|
+
if (this.holders.get(uri)?.sync) {
|
|
286
403
|
return undefined;
|
|
287
404
|
}
|
|
288
405
|
|
|
289
406
|
return this.service.closeDocument(uri);
|
|
290
407
|
})
|
|
291
408
|
.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
409
|
}
|
|
297
410
|
}
|
|
298
411
|
|
|
@@ -302,9 +415,16 @@ export class ProjectLanguage {
|
|
|
302
415
|
}
|
|
303
416
|
|
|
304
417
|
this.disposed = true;
|
|
418
|
+
for (const subscription of this.subscriptions) {
|
|
419
|
+
subscription.dispose();
|
|
420
|
+
}
|
|
305
421
|
disposeSnippetTransforms(this);
|
|
306
422
|
|
|
307
423
|
for (const [uri, holder] of this.holders) {
|
|
424
|
+
if (holder.sync === null) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
308
428
|
holder.sync.dispose();
|
|
309
429
|
|
|
310
430
|
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,
|
|
70
|
+
const { editor, uri } = this.language;
|
|
71
71
|
|
|
72
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
221
|
+
const { editor, uri } = this.language;
|
|
222
222
|
|
|
223
|
-
if (!
|
|
223
|
+
if (!this.language.service.supports('textDocument/references', uri)) {
|
|
224
224
|
return null;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
try {
|
|
228
|
-
const locations = await
|
|
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 {
|
|
280
|
+
const { uri } = this.language;
|
|
281
281
|
|
|
282
|
-
if (!
|
|
282
|
+
if (!this.language.service.supports('textDocument/documentSymbol', uri)) {
|
|
283
283
|
return undefined;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
try {
|
|
287
|
-
const symbols = await
|
|
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 {
|
package/src/rendered-docs.ts
CHANGED
|
@@ -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 {
|
|
257
|
+
const { uri } = this.language;
|
|
258
258
|
|
|
259
|
-
return
|
|
259
|
+
return this.language.service.supports('workspace/symbol', uri);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/* Goes to what a link in a comment names. */
|
package/src/selection-ranges.ts
CHANGED
|
@@ -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,
|
|
24
|
+
const { editor, uri } = language;
|
|
25
25
|
|
|
26
26
|
editor.setSelectionRanges(async (positions) => {
|
|
27
|
-
if (!
|
|
27
|
+
if (!language.service.supports(METHOD, uri)) {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const chains = await
|
|
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 {
|
|
115
|
-
const options =
|
|
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 {
|
|
150
|
+
const { uri } = this.language;
|
|
151
151
|
|
|
152
|
-
if (!
|
|
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,
|
|
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
|
|
171
|
+
help = await this.language.service.signatureHelp(uri, caret, context, { signal: controller.signal });
|
|
172
172
|
} catch {
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { SnippetTransformQueueError } from './snippet-transform-queue.ts';
|
|
2
|
+
import type { Editor, EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
|
+
import { comparePositions, shiftPosition } from './diagnostics-model.ts';
|
|
4
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { lineTextOf, textBeforeOf } from './line-text.ts';
|
|
6
|
+
import { wordRangeAt } from './rename-model.ts';
|
|
7
|
+
import { jobTable } from './snippet-field-transforms.ts';
|
|
8
|
+
import { indentSnippet } from './snippet-indent.ts';
|
|
9
|
+
import { positionInInsertion } from './snippet-session.ts';
|
|
10
|
+
import { SnippetTransformError } from './snippet-transform-client.ts';
|
|
11
|
+
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.ts';
|
|
12
|
+
import { compileSnippet, tabOrder, type ParsedSnippet, type SnippetTemplate } from './snippet.ts';
|
|
13
|
+
import { createSnippetResolver } from './snippet-variables.ts';
|
|
14
|
+
import { fileUriToPath } from '@adecore/lsp';
|
|
15
|
+
|
|
16
|
+
/* The same limits as an inserted completion item. */
|
|
17
|
+
const MAX_CHARACTERS = 2_000_000;
|
|
18
|
+
const MAX_RANGES = 1_000;
|
|
19
|
+
const TOAST_ID = 'language-completion';
|
|
20
|
+
|
|
21
|
+
export interface SnippetPlacement {
|
|
22
|
+
readonly range: EditorRange;
|
|
23
|
+
|
|
24
|
+
/* The value of `TM_SELECTED_TEXT` at this placement; empty without it. */
|
|
25
|
+
readonly selectedText?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class LimitError extends Error {}
|
|
29
|
+
|
|
30
|
+
interface Draft {
|
|
31
|
+
readonly placement: SnippetPlacement;
|
|
32
|
+
readonly resolve: (name: string) => string | undefined;
|
|
33
|
+
|
|
34
|
+
/* As rendered, which is the text the variable transforms read. */
|
|
35
|
+
raw: ParsedSnippet;
|
|
36
|
+
|
|
37
|
+
/* Indented to the text before the placement. */
|
|
38
|
+
parsed: ParsedSnippet;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Each line's leading tabs become the editor's own indentation unit. */
|
|
42
|
+
export function bodyWithIndentUnit(body: string, unit: string): string {
|
|
43
|
+
return unit === '\t' ? body : body.replace(/^\t+/gm, (tabs) => unit.repeat(tabs.length));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* One level of the editor's indentation: spaces for a tab size, or a tab. */
|
|
47
|
+
export function indentUnitOf(editor: Editor): string {
|
|
48
|
+
const { tabSize, insertSpaces } = editor.getIndentation();
|
|
49
|
+
|
|
50
|
+
return insertSpaces ? ' '.repeat(tabSize) : '\t';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* The lines after the first without `base` in front of them, or without the shortest indentation among them. */
|
|
54
|
+
export function dedentLines(text: string, base?: string): string {
|
|
55
|
+
const lines = text.split(/\r\n|\r|\n/).slice(1);
|
|
56
|
+
const indents = lines.filter((line) => line.trim() !== '').map((line) => /^[\t ]*/.exec(line)![0].length);
|
|
57
|
+
const removed = base?.length ?? (indents.length === 0 ? 0 : Math.min(...indents));
|
|
58
|
+
|
|
59
|
+
return removed === 0
|
|
60
|
+
? text
|
|
61
|
+
: text.replace(
|
|
62
|
+
/(\r\n|\r|\n)([^\r\n]*)/g,
|
|
63
|
+
(_match, lineBreak: string, line: string) => lineBreak + line.slice(Math.min(removed, /^[\t ]*/.exec(line)![0].length))
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sizeOf(parsed: ParsedSnippet): number {
|
|
68
|
+
return (
|
|
69
|
+
(parsed.transforms?.length ?? 0) +
|
|
70
|
+
(parsed.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? parsed.stops.length)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Inserts a snippet at every placement, the last of them the primary one, and starts a session on its tab stops. The
|
|
76
|
+
* variables are resolved for each placement (`extra` first, then the host's and the built-in ones), the lines are indented
|
|
77
|
+
* to the text before the placement, and a running session takes the insertion as a nested one. False when nothing was inserted.
|
|
78
|
+
*/
|
|
79
|
+
export async function insertSnippet(
|
|
80
|
+
language: EditorLanguage,
|
|
81
|
+
source: string,
|
|
82
|
+
placements: readonly SnippetPlacement[],
|
|
83
|
+
extra?: (name: string) => string | undefined
|
|
84
|
+
): Promise<boolean> {
|
|
85
|
+
const { editor, project, uri, snippets } = language;
|
|
86
|
+
|
|
87
|
+
if (placements.length === 0 || editor.isReadOnly?.() === true) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const template: SnippetTemplate = compileSnippet(source);
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const token = snippets.token;
|
|
94
|
+
const continuing = snippets.isActive;
|
|
95
|
+
let stale = false;
|
|
96
|
+
const off = editor.onTextChange(() => {
|
|
97
|
+
stale = true;
|
|
98
|
+
});
|
|
99
|
+
const current = (): boolean => !stale && snippets.token === token;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
let characters = 0;
|
|
103
|
+
let ranges = 0;
|
|
104
|
+
|
|
105
|
+
const drafts = placements.map((placement, index): Draft => {
|
|
106
|
+
const line = lineTextOf(editor, placement.range.start.line);
|
|
107
|
+
const word = wordRangeAt(line, placement.range.start);
|
|
108
|
+
|
|
109
|
+
const resolver = createSnippetResolver(
|
|
110
|
+
{
|
|
111
|
+
uri,
|
|
112
|
+
languageId: language.languageId,
|
|
113
|
+
position: placement.range.start,
|
|
114
|
+
selection: placement.range,
|
|
115
|
+
selectedText: placement.selectedText ?? '',
|
|
116
|
+
currentLine: line,
|
|
117
|
+
currentWord: word === null ? '' : editor.textInRange(word),
|
|
118
|
+
cursorIndex: index,
|
|
119
|
+
cursorCount: placements.length,
|
|
120
|
+
folder: project.host.folder,
|
|
121
|
+
path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
|
|
122
|
+
},
|
|
123
|
+
project.host.resolveSnippetVariable?.bind(project.host),
|
|
124
|
+
{ now }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const resolve = (name: string): string | undefined => extra?.(name) ?? resolver(name);
|
|
128
|
+
|
|
129
|
+
const raw = template.render(resolve, undefined, { indentVariables: true });
|
|
130
|
+
|
|
131
|
+
return { placement, resolve, raw, parsed: indentSnippet(raw, textBeforeOf(editor, placement.range.start)) };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
for (const { parsed } of drafts) {
|
|
135
|
+
characters += parsed.text.length;
|
|
136
|
+
ranges += sizeOf(parsed);
|
|
137
|
+
|
|
138
|
+
if (parsed.limited === true || characters > MAX_CHARACTERS || ranges > MAX_RANGES) {
|
|
139
|
+
throw new LimitError();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await transformVariables(language, template, drafts, current);
|
|
144
|
+
|
|
145
|
+
if (!current()) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const changes = drafts.map((draft): EditorContentChange => ({ range: draft.placement.range, text: draft.parsed.text }));
|
|
150
|
+
|
|
151
|
+
if (changes.reduce((total, change) => total + change.text.length, 0) > MAX_CHARACTERS) {
|
|
152
|
+
throw new LimitError();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Insertions before one another move it, so each start is mapped through the changes in front of it.
|
|
156
|
+
const starts = changes.map((change) =>
|
|
157
|
+
changes
|
|
158
|
+
.filter((other) => other !== change && comparePositions(other.range.end, change.range.start) <= 0)
|
|
159
|
+
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
160
|
+
.reduce((place, other) => shiftPosition(place, other), change.range.start)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (!(continuing ? snippets.applyEdits(changes, token) : editor.applyEdits(changes))) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
off();
|
|
168
|
+
place(language, drafts, starts, continuing && snippets.isActive && snippets.token === token);
|
|
169
|
+
editor.focus();
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof LimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
|
|
174
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.tooLarge') });
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (error instanceof SnippetTransformQueueError) {
|
|
180
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
|
|
181
|
+
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (error instanceof SnippetTransformError) {
|
|
186
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformFailed') });
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw error;
|
|
192
|
+
} finally {
|
|
193
|
+
off();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Runs the transforms of variables through the worker, once for all placements, and renders them again with the answers. */
|
|
198
|
+
async function transformVariables(language: EditorLanguage, template: SnippetTemplate, drafts: Draft[], current: () => boolean): Promise<void> {
|
|
199
|
+
const { editor, project, snippets } = language;
|
|
200
|
+
const { jobs, slotOf } = jobTable();
|
|
201
|
+
const slots = drafts.map((draft) =>
|
|
202
|
+
(draft.raw.transforms ?? []).map((range) => slotOf({ ...range.transform, value: draft.raw.text.slice(range.start, range.end) }))
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (jobs.length === 0 || project.host.snippetTransformWorker === undefined) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
|
|
210
|
+
throw new LimitError();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const values = await snippets.transformVariables(jobs, current);
|
|
214
|
+
|
|
215
|
+
if (values === null || !current()) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
drafts.forEach((draft, index) => {
|
|
220
|
+
const raw = template.render(
|
|
221
|
+
draft.resolve,
|
|
222
|
+
slots[index]!.map((slot) => values[slot]!),
|
|
223
|
+
{ indentVariables: true }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const parsed = indentSnippet(raw, textBeforeOf(editor, draft.placement.range.start));
|
|
227
|
+
|
|
228
|
+
if (parsed.limited === true) {
|
|
229
|
+
throw new LimitError();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
draft.raw = raw;
|
|
233
|
+
draft.parsed = parsed;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Starts the session on the tab stops, or puts the caret after the primary insertion when there are none. */
|
|
238
|
+
function place(language: EditorLanguage, drafts: readonly Draft[], starts: readonly EditorPosition[], nested: boolean): void {
|
|
239
|
+
const { editor, snippets } = language;
|
|
240
|
+
const primary = drafts.at(-1)!.parsed;
|
|
241
|
+
|
|
242
|
+
if (primary.stops.length > 0) {
|
|
243
|
+
snippets.beginInstances(
|
|
244
|
+
drafts.map((draft, index) => ({ start: starts[index]!, text: draft.parsed.text, stops: tabOrder(draft.parsed), fields: draft.parsed.fields })),
|
|
245
|
+
{ nested }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
editor.setSelections(
|
|
252
|
+
drafts.map((draft, index) => {
|
|
253
|
+
const end = positionInInsertion(starts[index]!, draft.parsed.text, draft.parsed.text.length);
|
|
254
|
+
|
|
255
|
+
return { start: end, end };
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
}
|