@adecore/editor-react 0.22.1 → 0.23.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.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +92 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PanelResizeHandle.d.ts +8 -0
- package/dist/PanelResizeHandle.js +71 -0
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +7 -4
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +93 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +398 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +15 -5
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +19 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +117 -3
- package/dist/locales/nl.json +117 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/panel-size.d.ts +6 -0
- package/dist/panel-size.js +25 -0
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.d.ts +3 -1
- package/dist/peek.js +6 -1
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +12 -0
- package/dist/project-language.js +52 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +64 -0
- package/dist/rendered-docs.js +374 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/timers.d.ts +1 -0
- package/dist/timers.js +4 -0
- package/dist/use-panel-body.d.ts +6 -0
- package/dist/use-panel-body.js +13 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +244 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PanelResizeHandle.tsx +104 -0
- package/src/PeekPanel.tsx +11 -6
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +151 -0
- package/src/call-hierarchy.ts +440 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +22 -6
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +23 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +117 -3
- package/src/locales/nl.json +117 -3
- package/src/models.ts +40 -1
- package/src/panel-size.ts +28 -0
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +6 -1
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +63 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +430 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/timers.ts +5 -0
- package/src/use-panel-body.ts +17 -0
- package/src/workspace-edit.ts +347 -169
package/src/completion.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SnippetTransformQueueError } from './snippet-transform-queue.ts';
|
|
2
|
+
import { SnippetTransformError } from './snippet-transform-client.ts';
|
|
3
|
+
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK, type SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
4
|
+
import { fileUriToPath, StaleResultError, type CompletionContext, type CompletionItem } from '@adecore/lsp';
|
|
2
5
|
import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
|
|
3
6
|
import { comparePositions, shiftPosition } from './diagnostics-model.ts';
|
|
4
7
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
8
|
import {
|
|
6
9
|
completionDocsOf,
|
|
10
|
+
editRangeOf,
|
|
7
11
|
identifierPrefix,
|
|
8
12
|
insertionOf,
|
|
9
13
|
isIdentifierCharacter,
|
|
@@ -18,6 +22,9 @@ import {
|
|
|
18
22
|
type Ranked
|
|
19
23
|
} from './completion-model.ts';
|
|
20
24
|
import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses, type CallPlan } from './completion-call.ts';
|
|
25
|
+
import { compileSnippet } from './snippet.ts';
|
|
26
|
+
import { createSnippetResolver } from './snippet-variables.ts';
|
|
27
|
+
import { wordRangeAt } from './rename-model.ts';
|
|
21
28
|
import { RecentChoices, recentChoicesOf } from './recent-choices.ts';
|
|
22
29
|
import { shikiLanguageOf } from './language-ids.ts';
|
|
23
30
|
import { textBeforeOf } from './line-text.ts';
|
|
@@ -34,6 +41,45 @@ const RESOLVE_DELAY_MS = 120;
|
|
|
34
41
|
const PAGE = 8;
|
|
35
42
|
const LINES_BACK = 60;
|
|
36
43
|
const TOAST_ID = 'language-completion';
|
|
44
|
+
const MAX_COMPLETION_CHARACTERS = 2_000_000;
|
|
45
|
+
const MAX_COMPLETION_RANGES = 1_000;
|
|
46
|
+
|
|
47
|
+
class CompletionLimitError extends Error {}
|
|
48
|
+
|
|
49
|
+
function mapCompletionEdits(
|
|
50
|
+
item: CompletionItem,
|
|
51
|
+
changes: readonly EditorContentChange[],
|
|
52
|
+
mapRange: (range: EditorRange) => EditorRange
|
|
53
|
+
): CompletionItem | null {
|
|
54
|
+
// Changes apply one after another, so each is checked against the range as the earlier ones left it.
|
|
55
|
+
const overlapsChange = (range: EditorRange): boolean => {
|
|
56
|
+
let { start, end } = range;
|
|
57
|
+
for (const change of changes) {
|
|
58
|
+
if (comparePositions(start, change.range.end) < 0 && comparePositions(change.range.start, end) < 0) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
start = shiftPosition(start, change);
|
|
62
|
+
end = shiftPosition(end, change);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
let valid = true;
|
|
67
|
+
const map = (range: EditorRange): EditorRange => {
|
|
68
|
+
valid &&= !overlapsChange(range);
|
|
69
|
+
return mapRange(range);
|
|
70
|
+
};
|
|
71
|
+
const edit = item.textEdit;
|
|
72
|
+
const mapped = {
|
|
73
|
+
...item,
|
|
74
|
+
...(edit === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { textEdit: 'range' in edit ? { ...edit, range: map(edit.range) } : { ...edit, insert: map(edit.insert), replace: map(edit.replace) } }),
|
|
77
|
+
...(item.additionalTextEdits === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { additionalTextEdits: item.additionalTextEdits.map((edit) => ({ ...edit, range: map(edit.range) })) })
|
|
80
|
+
};
|
|
81
|
+
return valid ? mapped : null;
|
|
82
|
+
}
|
|
37
83
|
|
|
38
84
|
/*
|
|
39
85
|
* The suggestions that open while a word is typed. A single typed identifier character or a trigger
|
|
@@ -60,6 +106,9 @@ export class CompletionFeature {
|
|
|
60
106
|
private requestTimer: unknown;
|
|
61
107
|
private refreshTimer: unknown;
|
|
62
108
|
private resolveTimer: unknown;
|
|
109
|
+
private textRevision = 0;
|
|
110
|
+
private acceptSequence = 0;
|
|
111
|
+
private disposed = false;
|
|
63
112
|
private readonly resolved = new Map<CompletionItem, CompletionItem>();
|
|
64
113
|
|
|
65
114
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
@@ -70,9 +119,13 @@ export class CompletionFeature {
|
|
|
70
119
|
editor.onKeyDown((event) => this.key(event)),
|
|
71
120
|
editor.onTextChange((change) => this.edited(change)),
|
|
72
121
|
editor.onCaret((position) => this.caretMoved(position)),
|
|
73
|
-
editor.onBlur(() =>
|
|
122
|
+
editor.onBlur(() => {
|
|
123
|
+
this.acceptSequence++;
|
|
124
|
+
this.close();
|
|
125
|
+
})
|
|
74
126
|
];
|
|
75
127
|
language.onDispose(() => {
|
|
128
|
+
this.disposed = true;
|
|
76
129
|
for (const off of offs) {
|
|
77
130
|
off();
|
|
78
131
|
}
|
|
@@ -86,6 +139,7 @@ export class CompletionFeature {
|
|
|
86
139
|
|
|
87
140
|
/* Asks for suggestions at the caret now, as Ctrl+Space does. */
|
|
88
141
|
invoke(): void {
|
|
142
|
+
this.language.pick.close();
|
|
89
143
|
this.explicit = true;
|
|
90
144
|
this.beginRequest({ triggerKind: 1 }, 0);
|
|
91
145
|
}
|
|
@@ -108,7 +162,38 @@ export class CompletionFeature {
|
|
|
108
162
|
if (entry === undefined) {
|
|
109
163
|
return;
|
|
110
164
|
}
|
|
111
|
-
const { editor, project, uri } = this.language;
|
|
165
|
+
const { editor, project, uri, snippets } = this.language;
|
|
166
|
+
const acceptSequence = ++this.acceptSequence;
|
|
167
|
+
const snippetToken = snippets.token;
|
|
168
|
+
const inSnippet = snippets.isActive;
|
|
169
|
+
const exitChanges: { changes: readonly EditorContentChange[]; map: (range: EditorRange) => EditorRange }[] = [];
|
|
170
|
+
if (snippets.hasPendingTransforms) {
|
|
171
|
+
this.close();
|
|
172
|
+
if (
|
|
173
|
+
!(await snippets.settleTransforms(
|
|
174
|
+
() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken,
|
|
175
|
+
(changes, map) => exitChanges.push({ changes, map })
|
|
176
|
+
))
|
|
177
|
+
) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const acceptedSelections = editor.getSelections();
|
|
182
|
+
const revision = this.textRevision;
|
|
183
|
+
const current = (): boolean => {
|
|
184
|
+
if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || (inSnippet && snippets.token !== snippetToken)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const selections = editor.getSelections();
|
|
188
|
+
return (
|
|
189
|
+
selections.length === acceptedSelections.length &&
|
|
190
|
+
selections.every(
|
|
191
|
+
(range, index) =>
|
|
192
|
+
comparePositions(range.start, acceptedSelections[index]!.start) === 0 &&
|
|
193
|
+
comparePositions(range.end, acceptedSelections[index]!.end) === 0
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
};
|
|
112
197
|
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
113
198
|
let item = this.resolved.get(entry.item) ?? entry.item;
|
|
114
199
|
const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
|
|
@@ -117,44 +202,214 @@ export class CompletionFeature {
|
|
|
117
202
|
// Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
|
|
118
203
|
item = await this.resolveNow(entry.item);
|
|
119
204
|
}
|
|
205
|
+
if (!current()) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const { changes, map } of exitChanges) {
|
|
209
|
+
const mapped = mapCompletionEdits(item, changes, map);
|
|
210
|
+
if (mapped === null) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
item = mapped;
|
|
214
|
+
}
|
|
120
215
|
const caret = editor.getCaret();
|
|
121
216
|
const lineBefore = textBeforeOf(editor, caret);
|
|
122
217
|
const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
|
|
123
|
-
// A character that commits is typed after the name, so a call is not added before it.
|
|
124
|
-
const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
|
|
125
|
-
const { before, rest } = this.surroundings(planned, caret, lineAfter);
|
|
126
|
-
const plan: CallPlan =
|
|
127
|
-
commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
|
|
128
|
-
const main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
|
|
129
|
-
const extras = (item.additionalTextEdits ?? []).map((edit): EditorContentChange => ({ range: edit.range, text: edit.newText }));
|
|
130
|
-
// An import above the insertion moves it, and the tab stops are measured from where it ends up.
|
|
131
|
-
const start = extras
|
|
132
|
-
.filter((extra) => comparePositions(extra.range.end, main.range.start) <= 0)
|
|
133
|
-
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
134
|
-
.reduce((place, extra) => shiftPosition(place, extra), main.range.start);
|
|
135
|
-
// A name typed at several carets is completed at all of them, and the other carets end behind what was put in.
|
|
136
218
|
const carets = editor.getSelections();
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
219
|
+
const source = item.textEdit?.newText ?? item.insertText ?? item.label;
|
|
220
|
+
if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
|
|
221
|
+
this.refuseLargeCompletion();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let characters = 0;
|
|
225
|
+
let ranges = 0;
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const template = item.insertTextFormat === 2 ? compileSnippet(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
|
|
228
|
+
const finishes = new Map<Insertion, (values: readonly string[]) => Insertion>();
|
|
229
|
+
const renderAt = (position: EditorPosition, selection: EditorRange): Insertion => {
|
|
230
|
+
const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
|
|
231
|
+
const word = wordRangeAt(line, position);
|
|
232
|
+
const resolver = createSnippetResolver(
|
|
233
|
+
{
|
|
234
|
+
uri,
|
|
235
|
+
languageId: this.language.languageId,
|
|
236
|
+
position,
|
|
237
|
+
selection,
|
|
238
|
+
selectedText: editor.textInRange(selection),
|
|
239
|
+
currentLine: line,
|
|
240
|
+
currentWord: word === null ? '' : editor.textInRange(word),
|
|
241
|
+
cursorIndex: Math.max(
|
|
242
|
+
0,
|
|
243
|
+
carets.findIndex((range) => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)
|
|
244
|
+
),
|
|
245
|
+
cursorCount: carets.length,
|
|
246
|
+
folder: project.host.folder,
|
|
247
|
+
path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
|
|
248
|
+
},
|
|
249
|
+
project.host.resolveSnippetVariable?.bind(project.host),
|
|
250
|
+
{ now }
|
|
251
|
+
);
|
|
252
|
+
const atItem =
|
|
253
|
+
position === caret || item.textEdit === undefined
|
|
254
|
+
? item
|
|
255
|
+
: { ...item, textEdit: undefined, insertText: item.textEdit?.newText ?? item.insertText };
|
|
256
|
+
const editStart = editRangeOf(atItem, replace)?.start;
|
|
257
|
+
// A commit character follows the name, so accepting must not also add a call.
|
|
258
|
+
const finish = (variableTransforms?: readonly string[]): Insertion => {
|
|
259
|
+
const insertion = insertionOf(
|
|
260
|
+
atItem,
|
|
261
|
+
position,
|
|
262
|
+
line.slice(0, position.character),
|
|
263
|
+
replace,
|
|
264
|
+
commit === undefined ? line.slice(position.character) : '(',
|
|
265
|
+
{
|
|
266
|
+
template,
|
|
267
|
+
variableTransforms,
|
|
268
|
+
resolve: resolver,
|
|
269
|
+
indentationBefore:
|
|
270
|
+
editStart === undefined ? undefined : editor.textInRange({ start: { line: editStart.line, character: 0 }, end: editStart })
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
return commit === undefined ? insertion : { ...insertion, text: insertion.text + commit };
|
|
274
|
+
};
|
|
275
|
+
const rendered = finish();
|
|
276
|
+
finishes.set(rendered, finish);
|
|
277
|
+
characters += rendered.text.length;
|
|
278
|
+
ranges +=
|
|
279
|
+
(rendered.variableTransforms?.length ?? 0) +
|
|
280
|
+
(rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
|
|
281
|
+
if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
|
|
282
|
+
throw new CompletionLimitError();
|
|
283
|
+
}
|
|
284
|
+
return rendered;
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
const planned = renderAt(caret, editor.getSelection());
|
|
288
|
+
const { before, rest } = this.surroundings(planned, caret, lineAfter);
|
|
289
|
+
const plan: CallPlan =
|
|
290
|
+
commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
|
|
291
|
+
let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
|
|
292
|
+
const extras = (item.additionalTextEdits ?? []).map((edit): EditorContentChange => ({ range: edit.range, text: edit.newText }));
|
|
293
|
+
const instances = new Map<EditorContentChange, Insertion>();
|
|
294
|
+
const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
|
|
295
|
+
const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main, continuing).map(
|
|
296
|
+
(insertion) => {
|
|
297
|
+
const selection = carets.find(
|
|
298
|
+
(range) =>
|
|
299
|
+
range.start.line === insertion.range.start.line &&
|
|
300
|
+
range.start.character >= insertion.range.start.character &&
|
|
301
|
+
range.start.character <= insertion.range.end.character
|
|
302
|
+
)!;
|
|
303
|
+
const position = selection[continuing ? 'end' : 'start'];
|
|
304
|
+
const own = renderAt(position, selection);
|
|
305
|
+
const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
|
|
306
|
+
const edit = { ...insertion, text: rendered.text };
|
|
307
|
+
const instance = { ...rendered, range: insertion.range };
|
|
308
|
+
instances.set(edit, instance);
|
|
309
|
+
const finish = finishes.get(own);
|
|
310
|
+
if (finish !== undefined) {
|
|
311
|
+
finishes.set(instance, (values) => ({ ...finish(values), range: insertion.range }));
|
|
312
|
+
}
|
|
313
|
+
return edit;
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
const drafts = [...mirrored.map((edit) => instances.get(edit)!), main];
|
|
317
|
+
const jobs: SnippetTransformJob[] = [];
|
|
318
|
+
const keys = new Map<string, number>();
|
|
319
|
+
const slots = drafts.map((draft) =>
|
|
320
|
+
(draft.variableTransforms ?? []).map((job) => {
|
|
321
|
+
const key = JSON.stringify(job);
|
|
322
|
+
let slot = keys.get(key);
|
|
323
|
+
if (slot === undefined) {
|
|
324
|
+
slot = jobs.length;
|
|
325
|
+
keys.set(key, slot);
|
|
326
|
+
jobs.push(job);
|
|
327
|
+
}
|
|
328
|
+
return slot;
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
|
|
332
|
+
throw new CompletionLimitError();
|
|
333
|
+
}
|
|
334
|
+
if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
|
|
335
|
+
const values = await snippets.transformVariables(jobs, current);
|
|
336
|
+
if (!current()) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (values !== null) {
|
|
340
|
+
const transformed = drafts.map((draft, index) =>
|
|
341
|
+
slots[index]!.length === 0 ? draft : finishes.get(draft)!(slots[index]!.map((slot) => values[slot]!))
|
|
342
|
+
);
|
|
343
|
+
if (
|
|
344
|
+
transformed.some((draft) => draft.limited) ||
|
|
345
|
+
transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS
|
|
346
|
+
) {
|
|
347
|
+
throw new CompletionLimitError();
|
|
348
|
+
}
|
|
349
|
+
main = transformed.at(-1)!;
|
|
350
|
+
mirrored.forEach((edit, index) => {
|
|
351
|
+
edit.text = transformed[index]!.text;
|
|
352
|
+
instances.set(edit, transformed[index]!);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
144
355
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
356
|
+
const primary = { range: main.range, text: main.text };
|
|
357
|
+
const changes = [primary, ...mirrored, ...extras];
|
|
358
|
+
if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
|
|
359
|
+
throw new CompletionLimitError();
|
|
148
360
|
}
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
361
|
+
// Imports and earlier carets move each insertion independently; the primary instance stays last.
|
|
362
|
+
const starts = [...mirrored, primary].map((insertion) =>
|
|
363
|
+
changes
|
|
364
|
+
.filter((change) => change !== insertion && comparePositions(change.range.end, insertion.range.start) <= 0)
|
|
365
|
+
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
366
|
+
.reduce((place, change) => shiftPosition(place, change), insertion.range.start)
|
|
367
|
+
);
|
|
368
|
+
const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
|
|
369
|
+
if (!applied) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const landed = editor.getSelections();
|
|
373
|
+
if (commit === undefined) {
|
|
374
|
+
this.language.snippets.beginInstances(
|
|
375
|
+
[...mirrored, primary].map((insertion, index) => {
|
|
376
|
+
const rendered = insertion === primary ? main : instances.get(insertion)!;
|
|
377
|
+
return { start: starts[index]!, text: rendered.text, stops: rendered.stops, fields: rendered.fields };
|
|
378
|
+
}),
|
|
379
|
+
{ nested: continuing && snippets.isActive }
|
|
380
|
+
);
|
|
381
|
+
if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
|
|
382
|
+
this.placeMirroredCarets(carets, mirrored, landed);
|
|
383
|
+
}
|
|
384
|
+
if (plan.action === 'enter') {
|
|
385
|
+
const end = editor.getCaret();
|
|
386
|
+
editor.setCaret({ line: end.line, character: end.character + plan.shift });
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
if (this.triggerCharacters().includes(commit)) {
|
|
390
|
+
this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
this.afterInsert(item, commit, lineBefore);
|
|
394
|
+
editor.focus();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (!current()) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (error instanceof SnippetTransformQueueError) {
|
|
400
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
|
|
401
|
+
} else if (error instanceof CompletionLimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
|
|
402
|
+
this.refuseLargeCompletion();
|
|
403
|
+
} else if (error instanceof SnippetTransformError) {
|
|
404
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformFailed') });
|
|
405
|
+
} else {
|
|
406
|
+
throw error;
|
|
154
407
|
}
|
|
155
408
|
}
|
|
156
|
-
|
|
157
|
-
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private refuseLargeCompletion(): void {
|
|
412
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.tooLarge') });
|
|
158
413
|
}
|
|
159
414
|
|
|
160
415
|
/* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
|
|
@@ -212,7 +467,7 @@ export class CompletionFeature {
|
|
|
212
467
|
private async resolveNow(item: CompletionItem): Promise<CompletionItem> {
|
|
213
468
|
const { editor, project, uri } = this.language;
|
|
214
469
|
try {
|
|
215
|
-
return await project.service.resolveCompletion(uri, item);
|
|
470
|
+
return { ...item, ...(await project.service.resolveCompletion(uri, item)) };
|
|
216
471
|
} catch (error) {
|
|
217
472
|
if (!(error instanceof StaleResultError)) {
|
|
218
473
|
return item;
|
|
@@ -222,7 +477,7 @@ export class CompletionFeature {
|
|
|
222
477
|
const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find(
|
|
223
478
|
(candidate) => candidate.label === item.label && candidate.kind === item.kind
|
|
224
479
|
);
|
|
225
|
-
return fresh === undefined ? item : await project.service.resolveCompletion(uri, fresh).catch(() => fresh);
|
|
480
|
+
return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh).catch(() => fresh)) };
|
|
226
481
|
} catch {
|
|
227
482
|
return item;
|
|
228
483
|
}
|
|
@@ -300,6 +555,7 @@ export class CompletionFeature {
|
|
|
300
555
|
}
|
|
301
556
|
|
|
302
557
|
private edited(change: EditorTextChange): void {
|
|
558
|
+
this.textRevision++;
|
|
303
559
|
if (change.source !== 'input' || change.changes.length !== 1) {
|
|
304
560
|
this.close();
|
|
305
561
|
return;
|
|
@@ -372,16 +628,20 @@ export class CompletionFeature {
|
|
|
372
628
|
this.controller?.abort();
|
|
373
629
|
const controller = new AbortController();
|
|
374
630
|
this.controller = controller;
|
|
375
|
-
let
|
|
631
|
+
let normalized: ReturnType<typeof itemsOf>;
|
|
376
632
|
try {
|
|
377
|
-
result = await project.service.completion(uri, position, context, { signal: controller.signal });
|
|
633
|
+
const result = await project.service.completion(uri, position, context, { signal: controller.signal });
|
|
634
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
normalized = itemsOf(result);
|
|
378
638
|
} catch {
|
|
639
|
+
if (request === this.request && this.controller === controller && !controller.signal.aborted) {
|
|
640
|
+
this.close();
|
|
641
|
+
}
|
|
379
642
|
return;
|
|
380
643
|
}
|
|
381
|
-
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const { items, incomplete } = itemsOf(result);
|
|
644
|
+
const { items, incomplete } = normalized;
|
|
385
645
|
this.pool = items;
|
|
386
646
|
this.incomplete = incomplete;
|
|
387
647
|
this.resolved.clear();
|
|
@@ -463,7 +723,7 @@ export class CompletionFeature {
|
|
|
463
723
|
void project.service
|
|
464
724
|
.resolveCompletion(uri, entry.item)
|
|
465
725
|
.then((item) => {
|
|
466
|
-
this.resolved.set(entry.item, item);
|
|
726
|
+
this.resolved.set(entry.item, { ...entry.item, ...item });
|
|
467
727
|
if (this.ranked[this.active]?.item === entry.item) {
|
|
468
728
|
this.publish();
|
|
469
729
|
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Diagnostic } from '@adecore/lsp';
|
|
|
2
2
|
import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { isShortcut } from './shortcut-keys.ts';
|
|
5
|
-
import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, shiftRange, type Problem } from './diagnostics-model.ts';
|
|
5
|
+
import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange, type Problem } from './diagnostics-model.ts';
|
|
6
6
|
|
|
7
7
|
export interface ProblemCounts {
|
|
8
8
|
readonly error: number;
|
|
@@ -69,7 +69,7 @@ export class DiagnosticsFeature {
|
|
|
69
69
|
return false;
|
|
70
70
|
}
|
|
71
71
|
const { start } = target.diagnostic.range;
|
|
72
|
-
this.reveal(
|
|
72
|
+
this.reveal(target, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -79,7 +79,7 @@ export class DiagnosticsFeature {
|
|
|
79
79
|
if (target === null) {
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
|
-
this.reveal(target
|
|
82
|
+
this.reveal(target, 'center');
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -90,11 +90,13 @@ export class DiagnosticsFeature {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
private reveal(
|
|
94
|
-
const { editor } = this.language;
|
|
95
|
-
editor.setCaret(
|
|
93
|
+
private reveal(problem: Problem, reveal: EditorReveal): void {
|
|
94
|
+
const { editor, project } = this.language;
|
|
95
|
+
editor.setCaret(problem.diagnostic.range.start, reveal);
|
|
96
96
|
editor.focus();
|
|
97
97
|
this.language.hover.showProblemsAt(editor.getCaret());
|
|
98
|
+
const severity = project.i18n.t(`editor:language.announce.severity.${severityOf(problem.diagnostic)}`);
|
|
99
|
+
editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
private shift(changes: readonly EditorContentChange[]): void {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { diffRows, type DiffDocument } from '@adecore/merge';
|
|
2
|
+
|
|
3
|
+
export interface DiffLimits {
|
|
4
|
+
readonly rows: number;
|
|
5
|
+
readonly characters: number;
|
|
6
|
+
readonly lineLength: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_UNFOLD_LIMITS: DiffLimits = { rows: 20000, characters: 2000000, lineLength: 10000 };
|
|
10
|
+
|
|
11
|
+
export function fitsDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, limits: DiffLimits, maxRows = limits.rows): boolean {
|
|
12
|
+
let rows = 0;
|
|
13
|
+
let characters = 0;
|
|
14
|
+
for (const row of diffRows(document, 'unified', unfolded)) {
|
|
15
|
+
if (row.kind === 'gap') {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const text = row.new === undefined ? document.sides.old[row.old!.index]! : document.sides.new[row.new.index]!;
|
|
19
|
+
characters += text.length + (rows > 0 ? 1 : 0);
|
|
20
|
+
if (++rows > maxRows || characters > limits.characters || text.length > limits.lineLength) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function expandDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, id: number, limits: DiffLimits): ReadonlyMap<number, number> {
|
|
28
|
+
const rows = diffRows(document, 'unified', unfolded);
|
|
29
|
+
const gap = rows.find((row) => row.kind === 'gap' && row.id === id);
|
|
30
|
+
if (gap?.kind !== 'gap' || !gap.unfoldable) {
|
|
31
|
+
return unfolded;
|
|
32
|
+
}
|
|
33
|
+
const visible = rows.filter((row) => row.kind === 'line').length;
|
|
34
|
+
const maxRows = Math.min(limits.rows, visible + 500);
|
|
35
|
+
const previous = unfolded.get(id) ?? 0;
|
|
36
|
+
let low = 0;
|
|
37
|
+
let high = Math.min(500, gap.oldCount);
|
|
38
|
+
// Between-hunk gaps reveal from both ends; search the per-end count within the shared row budget.
|
|
39
|
+
while (low < high) {
|
|
40
|
+
const count = Math.ceil((low + high) / 2);
|
|
41
|
+
const candidate = new Map(unfolded).set(id, previous + count);
|
|
42
|
+
if (fitsDiff(document, candidate, limits, maxRows)) {
|
|
43
|
+
low = count;
|
|
44
|
+
} else {
|
|
45
|
+
high = count - 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return low === 0 ? unfolded : new Map(unfolded).set(id, previous + low);
|
|
49
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Button } from '@adecore/ui';
|
|
5
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
6
|
+
import type { Editor, EditorEngine } from '@adecore/editor';
|
|
7
|
+
import type { DiffDisplay } from '@adecore/merge';
|
|
8
|
+
import { RowHost, LineActionHost } from './row-host.ts';
|
|
9
|
+
|
|
10
|
+
export interface MountedDiff {
|
|
11
|
+
engine: EditorEngine;
|
|
12
|
+
editor: Editor;
|
|
13
|
+
rows: RowHost;
|
|
14
|
+
actions: LineActionHost;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DiffOverlays({
|
|
18
|
+
mounted,
|
|
19
|
+
display,
|
|
20
|
+
unfold,
|
|
21
|
+
blocked
|
|
22
|
+
}: {
|
|
23
|
+
mounted: MountedDiff;
|
|
24
|
+
display: DiffDisplay;
|
|
25
|
+
unfold(id: number): void;
|
|
26
|
+
blocked?: ReadonlySet<number>;
|
|
27
|
+
}) {
|
|
28
|
+
const { t } = useTranslation('editor');
|
|
29
|
+
useSyncExternalStore(mounted.rows.subscribe, mounted.rows.getVersion, mounted.rows.getVersion);
|
|
30
|
+
useSyncExternalStore(mounted.actions.subscribe, mounted.actions.getVersion, mounted.actions.getVersion);
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{display.gaps.map(({ row }) => {
|
|
34
|
+
const container = mounted.rows.container(`gap-${row.id}`);
|
|
35
|
+
if (container === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
|
|
39
|
+
return createPortal(
|
|
40
|
+
<div className="adecore-editor-diff-gap">
|
|
41
|
+
{row.unfoldable ? (
|
|
42
|
+
<Button
|
|
43
|
+
size="xs"
|
|
44
|
+
disabled={blocked?.has(row.id)}
|
|
45
|
+
title={blocked?.has(row.id) ? t('diff.contextLimit') : undefined}
|
|
46
|
+
onClick={() => unfold(row.id)}
|
|
47
|
+
>
|
|
48
|
+
{text}
|
|
49
|
+
</Button>
|
|
50
|
+
) : (
|
|
51
|
+
<span>{text}</span>
|
|
52
|
+
)}
|
|
53
|
+
</div>,
|
|
54
|
+
container,
|
|
55
|
+
`gap-${row.id}`
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
{display.rows.map((row, index) => {
|
|
59
|
+
if (row.noNewline === undefined) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const container = mounted.actions.container(`no-newline-${index}`);
|
|
63
|
+
return container === undefined
|
|
64
|
+
? null
|
|
65
|
+
: createPortal(<span className="adecore-editor-diff-no-newline">{t('diff.noNewline')}</span>, container, `no-newline-${index}`);
|
|
66
|
+
})}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|