@adecore/editor-react 0.39.0 → 0.40.1
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 +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
package/src/code-vision.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { splitLines } from '@adecore/merge';
|
|
2
2
|
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
3
3
|
import type { EditorCodeVision, EditorCodeVisionEntry, EditorRect } from '@adecore/editor';
|
|
4
|
-
import type { DocumentSymbolResult } from '@adecore/lsp';
|
|
4
|
+
import type { DocumentSymbolResult, Location } from '@adecore/lsp';
|
|
5
5
|
import type { EditorLanguage } from './editor-language.ts';
|
|
6
6
|
import { type CodeAuthorship, authorsText, authorshipOf, mapBlame } from './code-authors.ts';
|
|
7
7
|
import { type CodeVisionDeclaration, declarationsOf, usagesText } from './code-vision-model.ts';
|
|
8
8
|
import { realTimers, type Timers } from './timers.ts';
|
|
9
|
+
import { placeKey } from './unique.ts';
|
|
9
10
|
|
|
10
11
|
const REFERENCES = 'textDocument/references';
|
|
12
|
+
|
|
11
13
|
/* Lines asked about above and below what is in view, so a little scrolling stays inside what is counted. */
|
|
12
14
|
const MARGIN_LINES = 40;
|
|
15
|
+
|
|
13
16
|
/* References are the slowest question a server is asked here, so only this many are in flight at once. */
|
|
14
17
|
const MAX_PARALLEL = 3;
|
|
18
|
+
|
|
15
19
|
const SCROLL_PAUSE_MS = 150;
|
|
20
|
+
|
|
16
21
|
/* Past this many lines a file gets no rows at all, which is also what keeps blame and the walk of symbols cheap. */
|
|
17
22
|
export const MAX_LINES = 20_000;
|
|
18
23
|
|
|
@@ -40,18 +45,25 @@ export class CodeVisionFeature {
|
|
|
40
45
|
private readonly timers: Timers;
|
|
41
46
|
private settings: CodeVisionSettings = { usages: false, authors: false };
|
|
42
47
|
private declarations: CodeVisionDeclaration[] = [];
|
|
48
|
+
|
|
43
49
|
/* What the declarations were read from, so a count never outlives the text it was asked for. */
|
|
44
50
|
private epoch = 0;
|
|
51
|
+
|
|
45
52
|
private edited = false;
|
|
46
53
|
private readonly counts = new Map<string, { count: number; epoch: number }>();
|
|
47
54
|
private queue: CodeVisionDeclaration[] = [];
|
|
55
|
+
|
|
48
56
|
/* Scrolling changes the pending queue but keeps requests already in flight. */
|
|
49
57
|
private readonly running = new Map<string, AbortController>();
|
|
58
|
+
|
|
50
59
|
/* Declarations a server could not count for this text, which are not asked again until it says it can. */
|
|
51
60
|
private failed = new Set<string>();
|
|
61
|
+
|
|
52
62
|
private blame: BlameSource | null = null;
|
|
63
|
+
|
|
53
64
|
/* The text the declarations were read from, which the lines of their authors are counted in. */
|
|
54
65
|
private lines: readonly string[] = [];
|
|
66
|
+
|
|
55
67
|
private mapped: { blame: GitBlameResult; lines: Int32Array | null } | null = null;
|
|
56
68
|
private readonly authorships = new Map<string, CodeAuthorship>();
|
|
57
69
|
private scrollTimer: unknown;
|
|
@@ -62,6 +74,7 @@ export class CodeVisionFeature {
|
|
|
62
74
|
this.language = language;
|
|
63
75
|
this.timers = timers;
|
|
64
76
|
const { editor } = language;
|
|
77
|
+
|
|
65
78
|
const offs = [
|
|
66
79
|
onSymbols((result) => this.read(result)),
|
|
67
80
|
editor.onTextChange(() => {
|
|
@@ -73,21 +86,27 @@ export class CodeVisionFeature {
|
|
|
73
86
|
this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
|
|
74
87
|
})
|
|
75
88
|
];
|
|
89
|
+
|
|
76
90
|
const providers = language.onProvidersChanged(() => {
|
|
77
91
|
this.failed.clear();
|
|
78
92
|
this.publish();
|
|
79
93
|
this.plan();
|
|
80
94
|
});
|
|
95
|
+
|
|
81
96
|
language.onDispose(() => {
|
|
82
97
|
this.disposed = true;
|
|
98
|
+
|
|
83
99
|
for (const off of offs) {
|
|
84
100
|
off();
|
|
85
101
|
}
|
|
102
|
+
|
|
86
103
|
providers();
|
|
87
104
|
this.timers.clear(this.scrollTimer);
|
|
105
|
+
|
|
88
106
|
for (const controller of this.running.values()) {
|
|
89
107
|
controller.abort();
|
|
90
108
|
}
|
|
109
|
+
|
|
91
110
|
this.closeAuthors();
|
|
92
111
|
editor.setCodeVision([]);
|
|
93
112
|
});
|
|
@@ -98,6 +117,7 @@ export class CodeVisionFeature {
|
|
|
98
117
|
if (settings.usages === this.settings.usages && settings.authors === this.settings.authors) {
|
|
99
118
|
return;
|
|
100
119
|
}
|
|
120
|
+
|
|
101
121
|
this.settings = settings;
|
|
102
122
|
this.publish();
|
|
103
123
|
this.plan();
|
|
@@ -118,8 +138,10 @@ export class CodeVisionFeature {
|
|
|
118
138
|
this.mapped = null;
|
|
119
139
|
this.authorships.clear();
|
|
120
140
|
this.edited = false;
|
|
141
|
+
|
|
121
142
|
this.declarations =
|
|
122
143
|
lines.length > MAX_LINES ? [] : declarationsOf(result, { lineCount: lines.length, lineAt: (line) => lines[line] ?? '', languageId });
|
|
144
|
+
|
|
123
145
|
this.epoch++;
|
|
124
146
|
this.failed.clear();
|
|
125
147
|
this.publish();
|
|
@@ -135,9 +157,11 @@ export class CodeVisionFeature {
|
|
|
135
157
|
if (this.disposed || this.edited || !this.canCount) {
|
|
136
158
|
return;
|
|
137
159
|
}
|
|
160
|
+
|
|
138
161
|
const view = this.language.editor.getVisibleRange();
|
|
139
162
|
const first = view.start.line - MARGIN_LINES;
|
|
140
163
|
const last = view.end.line + MARGIN_LINES;
|
|
164
|
+
|
|
141
165
|
this.queue = this.declarations
|
|
142
166
|
.filter(
|
|
143
167
|
(declaration) =>
|
|
@@ -149,6 +173,7 @@ export class CodeVisionFeature {
|
|
|
149
173
|
!this.failed.has(declaration.id)
|
|
150
174
|
)
|
|
151
175
|
.sort((left, right) => Math.abs(left.line - view.start.line) - Math.abs(right.line - view.start.line));
|
|
176
|
+
|
|
152
177
|
while (this.running.size < MAX_PARALLEL && this.queue.length > 0) {
|
|
153
178
|
void this.count(this.queue.shift()!);
|
|
154
179
|
}
|
|
@@ -159,8 +184,10 @@ export class CodeVisionFeature {
|
|
|
159
184
|
const controller = new AbortController();
|
|
160
185
|
this.running.set(declaration.id, controller);
|
|
161
186
|
const epoch = this.epoch;
|
|
187
|
+
|
|
162
188
|
try {
|
|
163
189
|
const locations = await project.service.references(uri, declaration.position, false, { signal: controller.signal, parallel: true });
|
|
190
|
+
|
|
164
191
|
if (!this.disposed && epoch === this.epoch) {
|
|
165
192
|
this.counts.set(declaration.id, { count: distinctPlaces(locations ?? []), epoch });
|
|
166
193
|
this.publish();
|
|
@@ -181,9 +208,12 @@ export class CodeVisionFeature {
|
|
|
181
208
|
if (this.publishing) {
|
|
182
209
|
return;
|
|
183
210
|
}
|
|
211
|
+
|
|
184
212
|
this.publishing = true;
|
|
213
|
+
|
|
185
214
|
queueMicrotask(() => {
|
|
186
215
|
this.publishing = false;
|
|
216
|
+
|
|
187
217
|
if (!this.disposed) {
|
|
188
218
|
this.language.editor.setCodeVision(this.rows());
|
|
189
219
|
}
|
|
@@ -197,14 +227,18 @@ export class CodeVisionFeature {
|
|
|
197
227
|
const withAuthors = this.settings.authors && this.blame !== null;
|
|
198
228
|
const commits = this.blame?.kind === 'ready' ? this.blame.blame.commits : [];
|
|
199
229
|
const mapped = this.blame?.kind === 'ready' && this.settings.authors ? this.mapOfBlame(this.blame) : null;
|
|
230
|
+
|
|
200
231
|
for (const declaration of this.declarations) {
|
|
201
232
|
const reserved = (counted && declaration.usages) || (withAuthors && declaration.authors);
|
|
233
|
+
|
|
202
234
|
if (!reserved || taken.has(declaration.line)) {
|
|
203
235
|
continue;
|
|
204
236
|
}
|
|
237
|
+
|
|
205
238
|
taken.add(declaration.line);
|
|
206
239
|
const entries: EditorCodeVisionEntry[] = [];
|
|
207
240
|
const known = this.counts.get(declaration.id);
|
|
241
|
+
|
|
208
242
|
if (counted && declaration.usages && known !== undefined) {
|
|
209
243
|
entries.push({
|
|
210
244
|
id: 'usages',
|
|
@@ -212,8 +246,10 @@ export class CodeVisionFeature {
|
|
|
212
246
|
activate: () => void this.language.peek.open(declaration.position)
|
|
213
247
|
});
|
|
214
248
|
}
|
|
249
|
+
|
|
215
250
|
if (mapped !== null && declaration.authors) {
|
|
216
251
|
const authorship = this.authorshipOf(declaration, commits, mapped);
|
|
252
|
+
|
|
217
253
|
entries.push({
|
|
218
254
|
id: 'authors',
|
|
219
255
|
text: authorsText(authorship, this.language.project.i18n),
|
|
@@ -221,18 +257,23 @@ export class CodeVisionFeature {
|
|
|
221
257
|
activate: (anchor) => this.showAuthors(declaration, authorship, anchor)
|
|
222
258
|
});
|
|
223
259
|
}
|
|
260
|
+
|
|
224
261
|
rows.push({ id: declaration.id, line: declaration.line, entries });
|
|
225
262
|
}
|
|
263
|
+
|
|
226
264
|
return rows;
|
|
227
265
|
}
|
|
228
266
|
|
|
229
267
|
private authorshipOf(declaration: CodeVisionDeclaration, commits: readonly GitBlameCommit[], mapped: Int32Array): CodeAuthorship {
|
|
230
268
|
const known = this.authorships.get(declaration.id);
|
|
269
|
+
|
|
231
270
|
if (known !== undefined) {
|
|
232
271
|
return known;
|
|
233
272
|
}
|
|
273
|
+
|
|
234
274
|
const authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
|
|
235
275
|
this.authorships.set(declaration.id, authorship);
|
|
276
|
+
|
|
236
277
|
return authorship;
|
|
237
278
|
}
|
|
238
279
|
|
|
@@ -240,12 +281,14 @@ export class CodeVisionFeature {
|
|
|
240
281
|
if (this.mapped?.blame !== source.blame) {
|
|
241
282
|
this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
|
|
242
283
|
}
|
|
284
|
+
|
|
243
285
|
return this.mapped.lines;
|
|
244
286
|
}
|
|
245
287
|
|
|
246
288
|
private showAuthors(declaration: CodeVisionDeclaration, authorship: CodeAuthorship, anchor: EditorRect): void {
|
|
247
289
|
const source = this.blame;
|
|
248
290
|
const openCommit = source?.kind === 'ready' ? source.openCommit : undefined;
|
|
291
|
+
|
|
249
292
|
this.language.popups.setState({
|
|
250
293
|
authors: {
|
|
251
294
|
anchor,
|
|
@@ -260,6 +303,7 @@ export class CodeVisionFeature {
|
|
|
260
303
|
closeAuthors(restoreFocus = false): void {
|
|
261
304
|
if (this.language.popups.getState().authors !== null) {
|
|
262
305
|
this.language.popups.setState({ authors: null });
|
|
306
|
+
|
|
263
307
|
if (restoreFocus) {
|
|
264
308
|
this.language.editor.focus();
|
|
265
309
|
}
|
|
@@ -268,6 +312,6 @@ export class CodeVisionFeature {
|
|
|
268
312
|
}
|
|
269
313
|
|
|
270
314
|
/* The places a server named, each counted once however many ways it was reached. */
|
|
271
|
-
function distinctPlaces(locations: readonly
|
|
272
|
-
return new Set(locations.map(
|
|
315
|
+
function distinctPlaces(locations: readonly Location[]): number {
|
|
316
|
+
return new Set(locations.map(placeKey)).size;
|
|
273
317
|
}
|
package/src/completion-call.ts
CHANGED
|
@@ -43,10 +43,13 @@ export function isCallItem(item: CompletionItem, lineBefore: string): boolean {
|
|
|
43
43
|
function listedParameters(signature: string): boolean | null {
|
|
44
44
|
const text = signature.replace(SIGNATURE_KIND_PREFIX, '');
|
|
45
45
|
const open = text.indexOf('(');
|
|
46
|
+
|
|
46
47
|
if (open < 0) {
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
50
|
+
|
|
49
51
|
let depth = 0;
|
|
52
|
+
|
|
50
53
|
for (let i = open; i < text.length; i++) {
|
|
51
54
|
if (text[i] === '(') {
|
|
52
55
|
depth++;
|
|
@@ -54,24 +57,29 @@ function listedParameters(signature: string): boolean | null {
|
|
|
54
57
|
return text.slice(open + 1, i).trim() !== '';
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
|
|
57
61
|
return null;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/* From what the server says of the item: `(method) Box.open(flag: boolean): void` has parameters and `(): void` has none. Null when it says nothing about them. */
|
|
61
65
|
export function hasParameters(item: CompletionItem): boolean | null {
|
|
62
66
|
const lines = (item.detail ?? '').split('\n').filter((line) => !SOURCE_LINE.test(line.trim()));
|
|
67
|
+
|
|
63
68
|
for (const signature of [item.labelDetails?.detail ?? '', ...lines]) {
|
|
64
69
|
const listed = listedParameters(signature);
|
|
70
|
+
|
|
65
71
|
if (listed !== null) {
|
|
66
72
|
return listed;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
75
|
+
|
|
69
76
|
return null;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/* The text before the innermost brace that is still open, which says whether the caret is in an import or export list. */
|
|
73
80
|
function beforeOpenBrace(before: string): string | null {
|
|
74
81
|
let depth = 0;
|
|
82
|
+
|
|
75
83
|
for (let i = before.length - 1; i >= 0; i--) {
|
|
76
84
|
if (before[i] === '}') {
|
|
77
85
|
depth++;
|
|
@@ -79,6 +87,7 @@ function beforeOpenBrace(before: string): string | null {
|
|
|
79
87
|
return before.slice(0, i);
|
|
80
88
|
}
|
|
81
89
|
}
|
|
90
|
+
|
|
82
91
|
return null;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -86,12 +95,15 @@ function beforeOpenBrace(before: string): string | null {
|
|
|
86
95
|
function isNameOnly(languageId: string, before: string): boolean {
|
|
87
96
|
const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
|
|
88
97
|
const brace = beforeOpenBrace(before);
|
|
98
|
+
|
|
89
99
|
if (brace !== null && (IMPORT_BRACE.test(brace) || (languageId === 'php' && PHP_USE_BRACE.test(brace)))) {
|
|
90
100
|
return true;
|
|
91
101
|
}
|
|
102
|
+
|
|
92
103
|
if (TYPE_OPERATOR.test(lineBefore) || /^\s*export\s+default\s+[\w$]*$/.test(lineBefore)) {
|
|
93
104
|
return true;
|
|
94
105
|
}
|
|
106
|
+
|
|
95
107
|
switch (languageId) {
|
|
96
108
|
case 'php':
|
|
97
109
|
return /^\s*use\s/.test(lineBefore) || /#\[[^\]]*$/.test(lineBefore);
|
|
@@ -111,19 +123,25 @@ function isNameOnly(languageId: string, before: string): boolean {
|
|
|
111
123
|
export function planCall(site: CallSite): CallPlan {
|
|
112
124
|
const { item, languageId, insertion, before, rest, replace } = site;
|
|
113
125
|
const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
|
|
126
|
+
|
|
114
127
|
if (!CALL_LANGUAGES.has(languageId) || !isCallItem(item, lineBefore) || item.insertTextFormat === SNIPPET_FORMAT || insertion.text.includes('(')) {
|
|
115
128
|
return { action: 'none' };
|
|
116
129
|
}
|
|
130
|
+
|
|
117
131
|
if (isNameOnly(languageId, before)) {
|
|
118
132
|
return { action: 'none' };
|
|
119
133
|
}
|
|
134
|
+
|
|
120
135
|
const existing = EXISTING_CALL.exec(rest);
|
|
136
|
+
|
|
121
137
|
if (existing !== null) {
|
|
122
138
|
return replace ? { action: 'enter', shift: existing[0].length } : { action: 'none' };
|
|
123
139
|
}
|
|
140
|
+
|
|
124
141
|
if (rest.startsWith('<')) {
|
|
125
142
|
return { action: 'none' };
|
|
126
143
|
}
|
|
144
|
+
|
|
127
145
|
return { action: 'add', inside: hasParameters(item) !== false };
|
|
128
146
|
}
|
|
129
147
|
|
|
@@ -131,5 +149,6 @@ export function planCall(site: CallSite): CallPlan {
|
|
|
131
149
|
export function withParentheses(insertion: Insertion, inside: boolean): Insertion {
|
|
132
150
|
const text = `${insertion.text}()`;
|
|
133
151
|
const caret = inside ? text.length - 1 : text.length;
|
|
152
|
+
|
|
134
153
|
return { range: insertion.range, text, stops: [{ index: 0, start: caret, end: caret }] };
|
|
135
154
|
}
|
package/src/completion-model.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { labelCollator } from '@adecore/ui/format';
|
|
|
2
2
|
import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
3
3
|
import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
4
4
|
import { applyCompletionDefaults, type CompletionItem, type CompletionResult, type MarkupContent } from '@adecore/lsp';
|
|
5
|
-
import { comparePositions } from './diagnostics-model.ts';
|
|
5
|
+
import { comparePositions, sameRange } from './diagnostics-model.ts';
|
|
6
6
|
import { escapeMarkdown, isEmptyHover, splitSignatures, type HoverText } from './hover-content.ts';
|
|
7
7
|
import type { CompletionDocs } from './popups.ts';
|
|
8
8
|
import { compileSnippet, parseSnippet, tabOrder, type SnippetField, type SnippetStop, type SnippetTemplate, type SnippetVariableResolver } from './snippet.ts';
|
|
@@ -22,18 +22,23 @@ export function identifierPrefix(before: string): string {
|
|
|
22
22
|
if (before === lastBefore) {
|
|
23
23
|
return lastPrefix;
|
|
24
24
|
}
|
|
25
|
+
|
|
25
26
|
const characters = Array.from(before);
|
|
26
27
|
let start = characters.length;
|
|
28
|
+
|
|
27
29
|
while (start > 0 && IDENTIFIER_CHARACTER.test(characters[start - 1]!)) {
|
|
28
30
|
start--;
|
|
29
31
|
}
|
|
32
|
+
|
|
30
33
|
lastBefore = before;
|
|
31
34
|
lastPrefix = characters.slice(start).join('');
|
|
35
|
+
|
|
32
36
|
return lastPrefix;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
const OPENS_VALUE =
|
|
36
40
|
/(?:[-=!<>&|?+*/%^:,]|(?:^|[^\p{L}\p{N}_$])(?:return|new|case|in|of|instanceof|typeof|keyof|await|yield|throw|extends|implements|as|is|not|and|or|echo|print|use|import|from|clone))$/u;
|
|
41
|
+
|
|
37
42
|
const NOT_A_VALUE = /(?:\+\+|--|\/\/|\/\*\*?|^\s*[-+*])$/;
|
|
38
43
|
|
|
39
44
|
/*
|
|
@@ -45,6 +50,7 @@ export function opensCompletion(before: string, text: string): boolean {
|
|
|
45
50
|
if (text === '(' || text === '()' || text === '[' || text === '[]') {
|
|
46
51
|
return true;
|
|
47
52
|
}
|
|
53
|
+
|
|
48
54
|
return text === ' ' && OPENS_VALUE.test(before) && !NOT_A_VALUE.test(before);
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -73,10 +79,13 @@ const shapes = new Map<string, LabelShape>();
|
|
|
73
79
|
/* What matching reads of a label, which is the same at every keystroke of a word and costs a regular expression pass to find. */
|
|
74
80
|
function shapeOf(value: string): LabelShape {
|
|
75
81
|
const known = shapes.get(value);
|
|
82
|
+
|
|
76
83
|
if (known !== undefined) {
|
|
77
84
|
return known;
|
|
78
85
|
}
|
|
86
|
+
|
|
79
87
|
const boundaries = [...value.matchAll(/(?:^|[_$\s-])([\p{L}\p{N}])|(\p{Lu})/gu)];
|
|
88
|
+
|
|
80
89
|
const shape: LabelShape = {
|
|
81
90
|
lower: value.toLocaleLowerCase(),
|
|
82
91
|
offsets: boundaries.map((boundary) => boundary.index! + (boundary[1] ? boundary[0].length - boundary[1].length : 0)),
|
|
@@ -85,10 +94,13 @@ function shapeOf(value: string): LabelShape {
|
|
|
85
94
|
.join('')
|
|
86
95
|
.toLocaleLowerCase()
|
|
87
96
|
};
|
|
97
|
+
|
|
88
98
|
if (shapes.size >= SHAPE_LIMIT) {
|
|
89
99
|
shapes.clear();
|
|
90
100
|
}
|
|
101
|
+
|
|
91
102
|
shapes.set(value, shape);
|
|
103
|
+
|
|
92
104
|
return shape;
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -102,35 +114,46 @@ export function matchDetail(value: string, prefix: string): MatchDetail | undefi
|
|
|
102
114
|
if (prefix === '') {
|
|
103
115
|
return { score: 0, positions: [] };
|
|
104
116
|
}
|
|
117
|
+
|
|
105
118
|
if (value.startsWith(prefix)) {
|
|
106
119
|
return { score: 0, positions: run(0, prefix.length) };
|
|
107
120
|
}
|
|
121
|
+
|
|
108
122
|
const { lower, offsets, initials } = shapeOf(value);
|
|
109
123
|
const query = prefix.toLocaleLowerCase();
|
|
124
|
+
|
|
110
125
|
if (lower.startsWith(query)) {
|
|
111
126
|
return { score: 1, positions: run(0, query.length) };
|
|
112
127
|
}
|
|
128
|
+
|
|
113
129
|
for (const offset of offsets) {
|
|
114
130
|
if (lower.slice(offset).startsWith(query)) {
|
|
115
131
|
return { score: 2 + offset / 1000, positions: run(offset, query.length) };
|
|
116
132
|
}
|
|
117
133
|
}
|
|
134
|
+
|
|
118
135
|
if (initials.startsWith(query)) {
|
|
119
136
|
return { score: 3, positions: offsets.slice(0, query.length) };
|
|
120
137
|
}
|
|
138
|
+
|
|
121
139
|
if (query.length < 2 || lower[0] !== query[0]) {
|
|
122
140
|
return undefined;
|
|
123
141
|
}
|
|
142
|
+
|
|
124
143
|
let cursor = 0;
|
|
125
144
|
const positions: number[] = [];
|
|
145
|
+
|
|
126
146
|
for (const character of query) {
|
|
127
147
|
const next = lower.indexOf(character, cursor);
|
|
148
|
+
|
|
128
149
|
if (next < 0) {
|
|
129
150
|
return undefined;
|
|
130
151
|
}
|
|
152
|
+
|
|
131
153
|
positions.push(next);
|
|
132
154
|
cursor = next + character.length;
|
|
133
155
|
}
|
|
156
|
+
|
|
134
157
|
return { score: 4 + cursor / 1000, positions };
|
|
135
158
|
}
|
|
136
159
|
|
|
@@ -141,12 +164,15 @@ export function matchScore(value: string, prefix: string): number | undefined {
|
|
|
141
164
|
/* The range an item replaces when it is accepted; for an insert-or-replace edit, the one `replace` asks for. */
|
|
142
165
|
export function editRangeOf(item: CompletionItem, replace: boolean): EditorRange | null {
|
|
143
166
|
const edit = item.textEdit;
|
|
167
|
+
|
|
144
168
|
if (edit === undefined) {
|
|
145
169
|
return null;
|
|
146
170
|
}
|
|
171
|
+
|
|
147
172
|
if ('range' in edit) {
|
|
148
173
|
return edit.range;
|
|
149
174
|
}
|
|
175
|
+
|
|
150
176
|
return replace ? edit.replace : edit.insert;
|
|
151
177
|
}
|
|
152
178
|
|
|
@@ -154,18 +180,21 @@ export function editRangeOf(item: CompletionItem, replace: boolean): EditorRange
|
|
|
154
180
|
export function itemsOf(result: CompletionResult): { items: CompletionItem[]; incomplete: boolean } {
|
|
155
181
|
const list = result === null || Array.isArray(result) ? undefined : result;
|
|
156
182
|
const items = result === null ? [] : Array.isArray(result) ? result : result.items;
|
|
183
|
+
|
|
157
184
|
return {
|
|
158
185
|
incomplete: list?.isIncomplete ?? false,
|
|
159
186
|
items: items.flatMap((item) => {
|
|
160
187
|
if (typeof item?.label !== 'string') {
|
|
161
188
|
return [];
|
|
162
189
|
}
|
|
190
|
+
|
|
163
191
|
try {
|
|
164
192
|
return [applyCompletionDefaults(item, list?.itemDefaults, { onInvalidRange: () => undefined })];
|
|
165
193
|
} catch (error) {
|
|
166
194
|
if (error instanceof TypeError || error instanceof RangeError) {
|
|
167
195
|
return [];
|
|
168
196
|
}
|
|
197
|
+
|
|
169
198
|
throw error;
|
|
170
199
|
}
|
|
171
200
|
})
|
|
@@ -179,9 +208,11 @@ export function itemsOf(result: CompletionResult): { items: CompletionItem[]; in
|
|
|
179
208
|
*/
|
|
180
209
|
export function prefixFor(item: CompletionItem, caret: EditorPosition, lineBefore: string, textBetween: (range: EditorRange) => string): string {
|
|
181
210
|
const range = editRangeOf(item, false);
|
|
211
|
+
|
|
182
212
|
if (range !== null && range.start.line === caret.line && comparePositions(range.start, caret) <= 0 && comparePositions(caret, range.end) <= 0) {
|
|
183
213
|
return textBetween({ start: range.start, end: caret });
|
|
184
214
|
}
|
|
215
|
+
|
|
185
216
|
return identifierPrefix(lineBefore);
|
|
186
217
|
}
|
|
187
218
|
|
|
@@ -203,9 +234,11 @@ export function rankCompletions(
|
|
|
203
234
|
limit = 150
|
|
204
235
|
): Ranked[] {
|
|
205
236
|
const { compare } = labelCollator();
|
|
237
|
+
|
|
206
238
|
return items
|
|
207
239
|
.map((item, index) => {
|
|
208
240
|
const prefix = prefixOf(item);
|
|
241
|
+
|
|
209
242
|
return { item, index, prefix, score: matchScore(item.filterText ?? item.label, prefix), recent: recency(item) };
|
|
210
243
|
})
|
|
211
244
|
.filter((entry): entry is typeof entry & { score: number } => entry.score !== undefined)
|
|
@@ -230,12 +263,14 @@ export function matchedCharacters(label: string, prefix: string): readonly numbe
|
|
|
230
263
|
function sourceOf(item: CompletionItem): string {
|
|
231
264
|
const detail = item.detail?.split('\n')[0]?.trim() ?? '';
|
|
232
265
|
const imported = /^use\s+(.+)$/.exec(detail);
|
|
266
|
+
|
|
233
267
|
return imported === null ? detail : imported[1]!.slice(0, Math.max(0, imported[1]!.lastIndexOf('\\')));
|
|
234
268
|
}
|
|
235
269
|
|
|
236
270
|
/* What stands right-aligned after a row: the module or namespace the item comes from, else what the server calls its type. */
|
|
237
271
|
export function qualifierOf(item: CompletionItem): string {
|
|
238
272
|
const description = item.labelDetails?.description?.trim() ?? '';
|
|
273
|
+
|
|
239
274
|
return description !== '' ? description : sourceOf(item);
|
|
240
275
|
}
|
|
241
276
|
|
|
@@ -247,16 +282,20 @@ export function qualifiersOf(items: readonly CompletionItem[]): string[] {
|
|
|
247
282
|
const result = items.map(qualifierOf);
|
|
248
283
|
const groups = new Map<string, number[]>();
|
|
249
284
|
items.forEach((item, index) => groups.set(item.label, [...(groups.get(item.label) ?? []), index]));
|
|
285
|
+
|
|
250
286
|
for (const indexes of groups.values()) {
|
|
251
287
|
if (indexes.length < 2 || new Set(indexes.map((index) => result[index])).size === indexes.length) {
|
|
252
288
|
continue;
|
|
253
289
|
}
|
|
290
|
+
|
|
254
291
|
const alternate = indexes.map((index) => sourceOf(items[index]!) || result[index]!);
|
|
255
292
|
const distinct = new Set(alternate).size === indexes.length && !alternate.includes('');
|
|
293
|
+
|
|
256
294
|
indexes.forEach((index, position) => {
|
|
257
295
|
result[index] = distinct ? alternate[position]! : `${result[index] ?? ''} #${position + 1}`.trim();
|
|
258
296
|
});
|
|
259
297
|
}
|
|
298
|
+
|
|
260
299
|
return result;
|
|
261
300
|
}
|
|
262
301
|
|
|
@@ -278,6 +317,7 @@ export function isCallableKind(kind: number | undefined): boolean {
|
|
|
278
317
|
/* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
|
|
279
318
|
function withoutCall(snippet: string): string {
|
|
280
319
|
const open = snippet.search(/(?<!\\)\(/);
|
|
320
|
+
|
|
281
321
|
return open > 0 ? snippet.slice(0, open) : snippet;
|
|
282
322
|
}
|
|
283
323
|
|
|
@@ -313,23 +353,28 @@ export function insertionOf(
|
|
|
313
353
|
): Insertion {
|
|
314
354
|
const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
|
|
315
355
|
const edit = editRangeOf(item, replace);
|
|
356
|
+
|
|
316
357
|
const range =
|
|
317
358
|
edit === null
|
|
318
359
|
? { start: { line: caret.line, character: caret.character - identifierPrefix(lineBefore).length }, end: caret }
|
|
319
360
|
: // The word grew since the answer came, so what was typed since belongs to what is replaced.
|
|
320
361
|
{ start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
|
|
362
|
+
|
|
321
363
|
if (item.insertTextFormat !== SNIPPET_FORMAT) {
|
|
322
364
|
const adjusted =
|
|
323
365
|
item.insertTextMode === 2
|
|
324
366
|
? indentSnippet({ text: raw, stops: [] }, options.indentationBefore ?? lineBefore.slice(0, range.start.character))
|
|
325
367
|
: { text: raw, stops: [] };
|
|
368
|
+
|
|
326
369
|
return { range, text: adjusted.text, stops: [], ...(adjusted.limited ? { limited: true } : {}) };
|
|
327
370
|
}
|
|
371
|
+
|
|
328
372
|
const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
|
|
329
373
|
const source = followed && isCallableKind(item.kind) ? withoutCall(raw) : raw;
|
|
330
374
|
const template = options.template?.source === source ? options.template : compileSnippet(source);
|
|
331
375
|
const rendered = template.render(options.resolve, options.variableTransforms);
|
|
332
376
|
const parsed = item.insertTextMode === 2 ? indentSnippet(rendered, options.indentationBefore ?? lineBefore.slice(0, range.start.character)) : rendered;
|
|
377
|
+
|
|
333
378
|
return {
|
|
334
379
|
range,
|
|
335
380
|
text: parsed.text,
|
|
@@ -358,23 +403,30 @@ export function mirroredInsertions(
|
|
|
358
403
|
): EditorContentChange[] {
|
|
359
404
|
const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
|
|
360
405
|
const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
|
|
406
|
+
|
|
361
407
|
if (before < 0 || after < 0) {
|
|
362
408
|
return [];
|
|
363
409
|
}
|
|
410
|
+
|
|
364
411
|
const typed = text({ start: insertion.range.start, end: caret });
|
|
365
412
|
const replaced = text({ start: caret, end: insertion.range.end });
|
|
413
|
+
|
|
366
414
|
return carets.flatMap((other): EditorContentChange[] => {
|
|
367
415
|
const at = includeSelections ? other.end : other.start;
|
|
368
|
-
|
|
416
|
+
|
|
417
|
+
if (sameRange(other, primary)) {
|
|
369
418
|
return [];
|
|
370
419
|
}
|
|
420
|
+
|
|
371
421
|
if ((!includeSelections && comparePositions(other.start, other.end) !== 0) || at.character < before) {
|
|
372
422
|
return [];
|
|
373
423
|
}
|
|
424
|
+
|
|
374
425
|
const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };
|
|
375
426
|
const preceding = range.start.character > 0 ? text({ start: { line: at.line, character: range.start.character - 1 }, end: range.start }) : '';
|
|
376
427
|
const startsWord = preceding === '' || !isIdentifierCharacter(preceding);
|
|
377
428
|
const matches = startsWord && text({ start: range.start, end: at }) === typed && text({ start: at, end: range.end }) === replaced;
|
|
429
|
+
|
|
378
430
|
return matches ? [{ range, text: insertion.text }] : [];
|
|
379
431
|
});
|
|
380
432
|
}
|
|
@@ -383,6 +435,7 @@ export function documentationText(documentation: string | MarkupContent | undefi
|
|
|
383
435
|
if (documentation === undefined) {
|
|
384
436
|
return '';
|
|
385
437
|
}
|
|
438
|
+
|
|
386
439
|
return typeof documentation === 'string' ? documentation : documentation.kind === 'plaintext' ? escapeMarkdown(documentation.value) : documentation.value;
|
|
387
440
|
}
|
|
388
441
|
|
|
@@ -400,10 +453,12 @@ export function completionDocsOf(item: CompletionItem, highlightLanguage: string
|
|
|
400
453
|
const source = hasSource ? lines[0]!.trim() : '';
|
|
401
454
|
const code = (hasSource ? lines.slice(1) : lines).join('\n').trim();
|
|
402
455
|
const parsed = splitSignatures(documentationText(item.documentation));
|
|
456
|
+
|
|
403
457
|
const text: HoverText = {
|
|
404
458
|
signatures: code === '' ? parsed.signatures : [{ language: highlightLanguage, code }, ...parsed.signatures],
|
|
405
459
|
markdown: parsed.markdown
|
|
406
460
|
};
|
|
461
|
+
|
|
407
462
|
return source === '' && isEmptyHover(text) ? null : { source, text };
|
|
408
463
|
}
|
|
409
464
|
|
|
@@ -475,6 +530,7 @@ export interface RowWindow {
|
|
|
475
530
|
*/
|
|
476
531
|
export function rowWindow(scrollTop: number, active: number, total: number, rowHeight: number, visible: number, margin: number, padding: number): RowWindow {
|
|
477
532
|
const shown = Math.floor(Math.max(0, scrollTop - padding) / rowHeight);
|
|
533
|
+
|
|
478
534
|
return {
|
|
479
535
|
first: Math.max(0, Math.min(shown, active) - margin),
|
|
480
536
|
last: Math.min(total, Math.max(shown + visible + 1, active + 1) + margin)
|