@adecore/editor-react 0.22.0 → 0.23.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.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -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/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- 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 +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -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 +14 -4
- 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 +17 -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 +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- 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.js +1 -0
- 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 +8 -0
- package/dist/project-language.js +34 -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 +62 -0
- package/dist/rendered-docs.js +368 -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/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -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/PeekPanel.tsx +2 -3
- 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 +158 -0
- package/src/call-hierarchy.ts +441 -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 +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -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 +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -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 +422 -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/workspace-edit.ts +347 -169
package/dist/completion.d.ts
CHANGED
|
@@ -18,12 +18,16 @@ export declare class CompletionFeature {
|
|
|
18
18
|
private requestTimer;
|
|
19
19
|
private refreshTimer;
|
|
20
20
|
private resolveTimer;
|
|
21
|
+
private textRevision;
|
|
22
|
+
private acceptSequence;
|
|
23
|
+
private disposed;
|
|
21
24
|
private readonly resolved;
|
|
22
25
|
constructor(language: EditorLanguage, timers?: Timers);
|
|
23
26
|
get isOpen(): boolean;
|
|
24
27
|
invoke(): void;
|
|
25
28
|
move(delta: number): void;
|
|
26
29
|
accept(replace: boolean, index?: number, commit?: string): Promise<void>;
|
|
30
|
+
private refuseLargeCompletion;
|
|
27
31
|
private placeMirroredCarets;
|
|
28
32
|
private surroundings;
|
|
29
33
|
private afterInsert;
|
package/dist/completion.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SnippetTransformQueueError } from './snippet-transform-queue.js';
|
|
2
|
+
import { SnippetTransformError } from './snippet-transform-client.js';
|
|
3
|
+
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.js';
|
|
4
|
+
import { fileUriToPath, StaleResultError } from '@adecore/lsp';
|
|
2
5
|
import { comparePositions, shiftPosition } from './diagnostics-model.js';
|
|
3
|
-
import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, opensCompletion, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
|
|
6
|
+
import { completionDocsOf, editRangeOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, opensCompletion, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
|
|
4
7
|
import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses } from './completion-call.js';
|
|
8
|
+
import { compileSnippet } from './snippet.js';
|
|
9
|
+
import { createSnippetResolver } from './snippet-variables.js';
|
|
10
|
+
import { wordRangeAt } from './rename-model.js';
|
|
5
11
|
import { RecentChoices, recentChoicesOf } from './recent-choices.js';
|
|
6
12
|
import { shikiLanguageOf } from './language-ids.js';
|
|
7
13
|
import { textBeforeOf } from './line-text.js';
|
|
@@ -16,6 +22,40 @@ const RESOLVE_DELAY_MS = 120;
|
|
|
16
22
|
const PAGE = 8;
|
|
17
23
|
const LINES_BACK = 60;
|
|
18
24
|
const TOAST_ID = 'language-completion';
|
|
25
|
+
const MAX_COMPLETION_CHARACTERS = 2_000_000;
|
|
26
|
+
const MAX_COMPLETION_RANGES = 1_000;
|
|
27
|
+
class CompletionLimitError extends Error {
|
|
28
|
+
}
|
|
29
|
+
function mapCompletionEdits(item, changes, mapRange) {
|
|
30
|
+
// Changes apply one after another, so each is checked against the range as the earlier ones left it.
|
|
31
|
+
const overlapsChange = (range) => {
|
|
32
|
+
let { start, end } = range;
|
|
33
|
+
for (const change of changes) {
|
|
34
|
+
if (comparePositions(start, change.range.end) < 0 && comparePositions(change.range.start, end) < 0) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
start = shiftPosition(start, change);
|
|
38
|
+
end = shiftPosition(end, change);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
let valid = true;
|
|
43
|
+
const map = (range) => {
|
|
44
|
+
valid &&= !overlapsChange(range);
|
|
45
|
+
return mapRange(range);
|
|
46
|
+
};
|
|
47
|
+
const edit = item.textEdit;
|
|
48
|
+
const mapped = {
|
|
49
|
+
...item,
|
|
50
|
+
...(edit === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { textEdit: 'range' in edit ? { ...edit, range: map(edit.range) } : { ...edit, insert: map(edit.insert), replace: map(edit.replace) } }),
|
|
53
|
+
...(item.additionalTextEdits === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { additionalTextEdits: item.additionalTextEdits.map((edit) => ({ ...edit, range: map(edit.range) })) })
|
|
56
|
+
};
|
|
57
|
+
return valid ? mapped : null;
|
|
58
|
+
}
|
|
19
59
|
/*
|
|
20
60
|
* The suggestions that open while a word is typed. A single typed identifier character or a trigger
|
|
21
61
|
* character of the server asks for the list; every character after that only filters the list that
|
|
@@ -41,6 +81,9 @@ export class CompletionFeature {
|
|
|
41
81
|
requestTimer;
|
|
42
82
|
refreshTimer;
|
|
43
83
|
resolveTimer;
|
|
84
|
+
textRevision = 0;
|
|
85
|
+
acceptSequence = 0;
|
|
86
|
+
disposed = false;
|
|
44
87
|
resolved = new Map();
|
|
45
88
|
constructor(language, timers = realTimers) {
|
|
46
89
|
this.language = language;
|
|
@@ -50,9 +93,13 @@ export class CompletionFeature {
|
|
|
50
93
|
editor.onKeyDown((event) => this.key(event)),
|
|
51
94
|
editor.onTextChange((change) => this.edited(change)),
|
|
52
95
|
editor.onCaret((position) => this.caretMoved(position)),
|
|
53
|
-
editor.onBlur(() =>
|
|
96
|
+
editor.onBlur(() => {
|
|
97
|
+
this.acceptSequence++;
|
|
98
|
+
this.close();
|
|
99
|
+
})
|
|
54
100
|
];
|
|
55
101
|
language.onDispose(() => {
|
|
102
|
+
this.disposed = true;
|
|
56
103
|
for (const off of offs) {
|
|
57
104
|
off();
|
|
58
105
|
}
|
|
@@ -64,6 +111,7 @@ export class CompletionFeature {
|
|
|
64
111
|
}
|
|
65
112
|
/* Asks for suggestions at the caret now, as Ctrl+Space does. */
|
|
66
113
|
invoke() {
|
|
114
|
+
this.language.pick.close();
|
|
67
115
|
this.explicit = true;
|
|
68
116
|
this.beginRequest({ triggerKind: 1 }, 0);
|
|
69
117
|
}
|
|
@@ -84,7 +132,28 @@ export class CompletionFeature {
|
|
|
84
132
|
if (entry === undefined) {
|
|
85
133
|
return;
|
|
86
134
|
}
|
|
87
|
-
const { editor, project, uri } = this.language;
|
|
135
|
+
const { editor, project, uri, snippets } = this.language;
|
|
136
|
+
const acceptSequence = ++this.acceptSequence;
|
|
137
|
+
const snippetToken = snippets.token;
|
|
138
|
+
const inSnippet = snippets.isActive;
|
|
139
|
+
const exitChanges = [];
|
|
140
|
+
if (snippets.hasPendingTransforms) {
|
|
141
|
+
this.close();
|
|
142
|
+
if (!(await snippets.settleTransforms(() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken, (changes, map) => exitChanges.push({ changes, map })))) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const acceptedSelections = editor.getSelections();
|
|
147
|
+
const revision = this.textRevision;
|
|
148
|
+
const current = () => {
|
|
149
|
+
if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || (inSnippet && snippets.token !== snippetToken)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const selections = editor.getSelections();
|
|
153
|
+
return (selections.length === acceptedSelections.length &&
|
|
154
|
+
selections.every((range, index) => comparePositions(range.start, acceptedSelections[index].start) === 0 &&
|
|
155
|
+
comparePositions(range.end, acceptedSelections[index].end) === 0));
|
|
156
|
+
};
|
|
88
157
|
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
89
158
|
let item = this.resolved.get(entry.item) ?? entry.item;
|
|
90
159
|
const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
|
|
@@ -93,44 +162,185 @@ export class CompletionFeature {
|
|
|
93
162
|
// Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
|
|
94
163
|
item = await this.resolveNow(entry.item);
|
|
95
164
|
}
|
|
165
|
+
if (!current()) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const { changes, map } of exitChanges) {
|
|
169
|
+
const mapped = mapCompletionEdits(item, changes, map);
|
|
170
|
+
if (mapped === null) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
item = mapped;
|
|
174
|
+
}
|
|
96
175
|
const caret = editor.getCaret();
|
|
97
176
|
const lineBefore = textBeforeOf(editor, caret);
|
|
98
177
|
const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
|
|
99
|
-
// A character that commits is typed after the name, so a call is not added before it.
|
|
100
|
-
const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
|
|
101
|
-
const { before, rest } = this.surroundings(planned, caret, lineAfter);
|
|
102
|
-
const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
|
|
103
|
-
const main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
|
|
104
|
-
const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
|
|
105
|
-
// An import above the insertion moves it, and the tab stops are measured from where it ends up.
|
|
106
|
-
const start = extras
|
|
107
|
-
.filter((extra) => comparePositions(extra.range.end, main.range.start) <= 0)
|
|
108
|
-
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
109
|
-
.reduce((place, extra) => shiftPosition(place, extra), main.range.start);
|
|
110
|
-
// A name typed at several carets is completed at all of them, and the other carets end behind what was put in.
|
|
111
178
|
const carets = editor.getSelections();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
const source = item.textEdit?.newText ?? item.insertText ?? item.label;
|
|
180
|
+
if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
|
|
181
|
+
this.refuseLargeCompletion();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let characters = 0;
|
|
185
|
+
let ranges = 0;
|
|
186
|
+
const now = new Date();
|
|
187
|
+
const template = item.insertTextFormat === 2 ? compileSnippet(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
|
|
188
|
+
const finishes = new Map();
|
|
189
|
+
const renderAt = (position, selection) => {
|
|
190
|
+
const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
|
|
191
|
+
const word = wordRangeAt(line, position);
|
|
192
|
+
const resolver = createSnippetResolver({
|
|
193
|
+
uri,
|
|
194
|
+
languageId: this.language.languageId,
|
|
195
|
+
position,
|
|
196
|
+
selection,
|
|
197
|
+
selectedText: editor.textInRange(selection),
|
|
198
|
+
currentLine: line,
|
|
199
|
+
currentWord: word === null ? '' : editor.textInRange(word),
|
|
200
|
+
cursorIndex: Math.max(0, carets.findIndex((range) => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)),
|
|
201
|
+
cursorCount: carets.length,
|
|
202
|
+
folder: project.host.folder,
|
|
203
|
+
path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
|
|
204
|
+
}, project.host.resolveSnippetVariable?.bind(project.host), { now });
|
|
205
|
+
const atItem = position === caret || item.textEdit === undefined
|
|
206
|
+
? item
|
|
207
|
+
: { ...item, textEdit: undefined, insertText: item.textEdit?.newText ?? item.insertText };
|
|
208
|
+
const editStart = editRangeOf(atItem, replace)?.start;
|
|
209
|
+
// A commit character follows the name, so accepting must not also add a call.
|
|
210
|
+
const finish = (variableTransforms) => {
|
|
211
|
+
const insertion = insertionOf(atItem, position, line.slice(0, position.character), replace, commit === undefined ? line.slice(position.character) : '(', {
|
|
212
|
+
template,
|
|
213
|
+
variableTransforms,
|
|
214
|
+
resolve: resolver,
|
|
215
|
+
indentationBefore: editStart === undefined ? undefined : editor.textInRange({ start: { line: editStart.line, character: 0 }, end: editStart })
|
|
216
|
+
});
|
|
217
|
+
return commit === undefined ? insertion : { ...insertion, text: insertion.text + commit };
|
|
218
|
+
};
|
|
219
|
+
const rendered = finish();
|
|
220
|
+
finishes.set(rendered, finish);
|
|
221
|
+
characters += rendered.text.length;
|
|
222
|
+
ranges +=
|
|
223
|
+
(rendered.variableTransforms?.length ?? 0) +
|
|
224
|
+
(rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
|
|
225
|
+
if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
|
|
226
|
+
throw new CompletionLimitError();
|
|
119
227
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
228
|
+
return rendered;
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
const planned = renderAt(caret, editor.getSelection());
|
|
232
|
+
const { before, rest } = this.surroundings(planned, caret, lineAfter);
|
|
233
|
+
const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
|
|
234
|
+
let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
|
|
235
|
+
const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
|
|
236
|
+
const instances = new Map();
|
|
237
|
+
const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
|
|
238
|
+
const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main, continuing).map((insertion) => {
|
|
239
|
+
const selection = carets.find((range) => range.start.line === insertion.range.start.line &&
|
|
240
|
+
range.start.character >= insertion.range.start.character &&
|
|
241
|
+
range.start.character <= insertion.range.end.character);
|
|
242
|
+
const position = selection[continuing ? 'end' : 'start'];
|
|
243
|
+
const own = renderAt(position, selection);
|
|
244
|
+
const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
|
|
245
|
+
const edit = { ...insertion, text: rendered.text };
|
|
246
|
+
const instance = { ...rendered, range: insertion.range };
|
|
247
|
+
instances.set(edit, instance);
|
|
248
|
+
const finish = finishes.get(own);
|
|
249
|
+
if (finish !== undefined) {
|
|
250
|
+
finishes.set(instance, (values) => ({ ...finish(values), range: insertion.range }));
|
|
251
|
+
}
|
|
252
|
+
return edit;
|
|
253
|
+
});
|
|
254
|
+
const drafts = [...mirrored.map((edit) => instances.get(edit)), main];
|
|
255
|
+
const jobs = [];
|
|
256
|
+
const keys = new Map();
|
|
257
|
+
const slots = drafts.map((draft) => (draft.variableTransforms ?? []).map((job) => {
|
|
258
|
+
const key = JSON.stringify(job);
|
|
259
|
+
let slot = keys.get(key);
|
|
260
|
+
if (slot === undefined) {
|
|
261
|
+
slot = jobs.length;
|
|
262
|
+
keys.set(key, slot);
|
|
263
|
+
jobs.push(job);
|
|
264
|
+
}
|
|
265
|
+
return slot;
|
|
266
|
+
}));
|
|
267
|
+
if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
|
|
268
|
+
throw new CompletionLimitError();
|
|
123
269
|
}
|
|
270
|
+
if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
|
|
271
|
+
const values = await snippets.transformVariables(jobs, current);
|
|
272
|
+
if (!current()) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (values !== null) {
|
|
276
|
+
const transformed = drafts.map((draft, index) => slots[index].length === 0 ? draft : finishes.get(draft)(slots[index].map((slot) => values[slot])));
|
|
277
|
+
if (transformed.some((draft) => draft.limited) ||
|
|
278
|
+
transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
|
|
279
|
+
throw new CompletionLimitError();
|
|
280
|
+
}
|
|
281
|
+
main = transformed.at(-1);
|
|
282
|
+
mirrored.forEach((edit, index) => {
|
|
283
|
+
edit.text = transformed[index].text;
|
|
284
|
+
instances.set(edit, transformed[index]);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const primary = { range: main.range, text: main.text };
|
|
289
|
+
const changes = [primary, ...mirrored, ...extras];
|
|
290
|
+
if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
|
|
291
|
+
throw new CompletionLimitError();
|
|
292
|
+
}
|
|
293
|
+
// Imports and earlier carets move each insertion independently; the primary instance stays last.
|
|
294
|
+
const starts = [...mirrored, primary].map((insertion) => changes
|
|
295
|
+
.filter((change) => change !== insertion && comparePositions(change.range.end, insertion.range.start) <= 0)
|
|
296
|
+
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
297
|
+
.reduce((place, change) => shiftPosition(place, change), insertion.range.start));
|
|
298
|
+
const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
|
|
299
|
+
if (!applied) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const landed = editor.getSelections();
|
|
303
|
+
if (commit === undefined) {
|
|
304
|
+
this.language.snippets.beginInstances([...mirrored, primary].map((insertion, index) => {
|
|
305
|
+
const rendered = insertion === primary ? main : instances.get(insertion);
|
|
306
|
+
return { start: starts[index], text: rendered.text, stops: rendered.stops, fields: rendered.fields };
|
|
307
|
+
}), { nested: continuing && snippets.isActive });
|
|
308
|
+
if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
|
|
309
|
+
this.placeMirroredCarets(carets, mirrored, landed);
|
|
310
|
+
}
|
|
311
|
+
if (plan.action === 'enter') {
|
|
312
|
+
const end = editor.getCaret();
|
|
313
|
+
editor.setCaret({ line: end.line, character: end.character + plan.shift });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
if (this.triggerCharacters().includes(commit)) {
|
|
318
|
+
this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.afterInsert(item, commit, lineBefore);
|
|
322
|
+
editor.focus();
|
|
124
323
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (!current()) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (error instanceof SnippetTransformQueueError) {
|
|
329
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
|
|
330
|
+
}
|
|
331
|
+
else if (error instanceof CompletionLimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
|
|
332
|
+
this.refuseLargeCompletion();
|
|
333
|
+
}
|
|
334
|
+
else if (error instanceof SnippetTransformError) {
|
|
335
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformFailed') });
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
throw error;
|
|
130
339
|
}
|
|
131
340
|
}
|
|
132
|
-
|
|
133
|
-
|
|
341
|
+
}
|
|
342
|
+
refuseLargeCompletion() {
|
|
343
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.tooLarge') });
|
|
134
344
|
}
|
|
135
345
|
/* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
|
|
136
346
|
placeMirroredCarets(before, mirrored, landed) {
|
|
@@ -183,7 +393,7 @@ export class CompletionFeature {
|
|
|
183
393
|
async resolveNow(item) {
|
|
184
394
|
const { editor, project, uri } = this.language;
|
|
185
395
|
try {
|
|
186
|
-
return await project.service.resolveCompletion(uri, item);
|
|
396
|
+
return { ...item, ...(await project.service.resolveCompletion(uri, item)) };
|
|
187
397
|
}
|
|
188
398
|
catch (error) {
|
|
189
399
|
if (!(error instanceof StaleResultError)) {
|
|
@@ -192,7 +402,7 @@ export class CompletionFeature {
|
|
|
192
402
|
}
|
|
193
403
|
try {
|
|
194
404
|
const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
|
|
195
|
-
return fresh === undefined ? item : await project.service.resolveCompletion(uri, fresh).catch(() => fresh);
|
|
405
|
+
return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh).catch(() => fresh)) };
|
|
196
406
|
}
|
|
197
407
|
catch {
|
|
198
408
|
return item;
|
|
@@ -268,6 +478,7 @@ export class CompletionFeature {
|
|
|
268
478
|
return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
|
|
269
479
|
}
|
|
270
480
|
edited(change) {
|
|
481
|
+
this.textRevision++;
|
|
271
482
|
if (change.source !== 'input' || change.changes.length !== 1) {
|
|
272
483
|
this.close();
|
|
273
484
|
return;
|
|
@@ -340,17 +551,21 @@ export class CompletionFeature {
|
|
|
340
551
|
this.controller?.abort();
|
|
341
552
|
const controller = new AbortController();
|
|
342
553
|
this.controller = controller;
|
|
343
|
-
let
|
|
554
|
+
let normalized;
|
|
344
555
|
try {
|
|
345
|
-
result = await project.service.completion(uri, position, context, { signal: controller.signal });
|
|
556
|
+
const result = await project.service.completion(uri, position, context, { signal: controller.signal });
|
|
557
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
normalized = itemsOf(result);
|
|
346
561
|
}
|
|
347
562
|
catch {
|
|
563
|
+
if (request === this.request && this.controller === controller && !controller.signal.aborted) {
|
|
564
|
+
this.close();
|
|
565
|
+
}
|
|
348
566
|
return;
|
|
349
567
|
}
|
|
350
|
-
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
const { items, incomplete } = itemsOf(result);
|
|
568
|
+
const { items, incomplete } = normalized;
|
|
354
569
|
this.pool = items;
|
|
355
570
|
this.incomplete = incomplete;
|
|
356
571
|
this.resolved.clear();
|
|
@@ -428,7 +643,7 @@ export class CompletionFeature {
|
|
|
428
643
|
void project.service
|
|
429
644
|
.resolveCompletion(uri, entry.item)
|
|
430
645
|
.then((item) => {
|
|
431
|
-
this.resolved.set(entry.item, item);
|
|
646
|
+
this.resolved.set(entry.item, { ...entry.item, ...item });
|
|
432
647
|
if (this.ranked[this.active]?.item === entry.item) {
|
|
433
648
|
this.publish();
|
|
434
649
|
}
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isShortcut } from './shortcut-keys.js';
|
|
2
|
-
import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, shiftRange } from './diagnostics-model.js';
|
|
2
|
+
import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange } from './diagnostics-model.js';
|
|
3
3
|
/*
|
|
4
4
|
* The problems the servers report for one file. A report from a server replaces the one before it
|
|
5
5
|
* from the same server, the editor draws them, and each one follows its text through edits made
|
|
@@ -54,7 +54,7 @@ export class DiagnosticsFeature {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
const { start } = target.diagnostic.range;
|
|
57
|
-
this.reveal(
|
|
57
|
+
this.reveal(target, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
/* Puts the caret on the first problem of the file. */
|
|
@@ -63,7 +63,7 @@ export class DiagnosticsFeature {
|
|
|
63
63
|
if (target === null) {
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
|
-
this.reveal(target
|
|
66
|
+
this.reveal(target, 'center');
|
|
67
67
|
return true;
|
|
68
68
|
}
|
|
69
69
|
onChange(listener) {
|
|
@@ -72,11 +72,13 @@ export class DiagnosticsFeature {
|
|
|
72
72
|
this.listeners.delete(listener);
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
reveal(
|
|
76
|
-
const { editor } = this.language;
|
|
77
|
-
editor.setCaret(
|
|
75
|
+
reveal(problem, reveal) {
|
|
76
|
+
const { editor, project } = this.language;
|
|
77
|
+
editor.setCaret(problem.diagnostic.range.start, reveal);
|
|
78
78
|
editor.focus();
|
|
79
79
|
this.language.hover.showProblemsAt(editor.getCaret());
|
|
80
|
+
const severity = project.i18n.t(`editor:language.announce.severity.${severityOf(problem.diagnostic)}`);
|
|
81
|
+
editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
|
|
80
82
|
}
|
|
81
83
|
shift(changes) {
|
|
82
84
|
for (const [server, diagnostics] of this.reports) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DiffDocument } from '@adecore/merge';
|
|
2
|
+
export interface DiffLimits {
|
|
3
|
+
readonly rows: number;
|
|
4
|
+
readonly characters: number;
|
|
5
|
+
readonly lineLength: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_UNFOLD_LIMITS: DiffLimits;
|
|
8
|
+
export declare function fitsDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, limits: DiffLimits, maxRows?: number): boolean;
|
|
9
|
+
export declare function expandDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, id: number, limits: DiffLimits): ReadonlyMap<number, number>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { diffRows } from '@adecore/merge';
|
|
2
|
+
export const DEFAULT_UNFOLD_LIMITS = { rows: 20000, characters: 2000000, lineLength: 10000 };
|
|
3
|
+
export function fitsDiff(document, unfolded, limits, maxRows = limits.rows) {
|
|
4
|
+
let rows = 0;
|
|
5
|
+
let characters = 0;
|
|
6
|
+
for (const row of diffRows(document, 'unified', unfolded)) {
|
|
7
|
+
if (row.kind === 'gap') {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const text = row.new === undefined ? document.sides.old[row.old.index] : document.sides.new[row.new.index];
|
|
11
|
+
characters += text.length + (rows > 0 ? 1 : 0);
|
|
12
|
+
if (++rows > maxRows || characters > limits.characters || text.length > limits.lineLength) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export function expandDiff(document, unfolded, id, limits) {
|
|
19
|
+
const rows = diffRows(document, 'unified', unfolded);
|
|
20
|
+
const gap = rows.find((row) => row.kind === 'gap' && row.id === id);
|
|
21
|
+
if (gap?.kind !== 'gap' || !gap.unfoldable) {
|
|
22
|
+
return unfolded;
|
|
23
|
+
}
|
|
24
|
+
const visible = rows.filter((row) => row.kind === 'line').length;
|
|
25
|
+
const maxRows = Math.min(limits.rows, visible + 500);
|
|
26
|
+
const previous = unfolded.get(id) ?? 0;
|
|
27
|
+
let low = 0;
|
|
28
|
+
let high = Math.min(500, gap.oldCount);
|
|
29
|
+
// Between-hunk gaps reveal from both ends; search the per-end count within the shared row budget.
|
|
30
|
+
while (low < high) {
|
|
31
|
+
const count = Math.ceil((low + high) / 2);
|
|
32
|
+
const candidate = new Map(unfolded).set(id, previous + count);
|
|
33
|
+
if (fitsDiff(document, candidate, limits, maxRows)) {
|
|
34
|
+
low = count;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
high = count - 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return low === 0 ? unfolded : new Map(unfolded).set(id, previous + low);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Editor, EditorEngine } from '@adecore/editor';
|
|
2
|
+
import type { DiffDisplay } from '@adecore/merge';
|
|
3
|
+
import { RowHost, LineActionHost } from './row-host.ts';
|
|
4
|
+
export interface MountedDiff {
|
|
5
|
+
engine: EditorEngine;
|
|
6
|
+
editor: Editor;
|
|
7
|
+
rows: RowHost;
|
|
8
|
+
actions: LineActionHost;
|
|
9
|
+
}
|
|
10
|
+
export declare function DiffOverlays({ mounted, display, unfold, blocked }: {
|
|
11
|
+
mounted: MountedDiff;
|
|
12
|
+
display: DiffDisplay;
|
|
13
|
+
unfold(id: number): void;
|
|
14
|
+
blocked?: ReadonlySet<number>;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Button } from '@adecore/ui';
|
|
6
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { RowHost, LineActionHost } from './row-host.js';
|
|
8
|
+
export function DiffOverlays({ mounted, display, unfold, blocked }) {
|
|
9
|
+
const { t } = useTranslation('editor');
|
|
10
|
+
useSyncExternalStore(mounted.rows.subscribe, mounted.rows.getVersion, mounted.rows.getVersion);
|
|
11
|
+
useSyncExternalStore(mounted.actions.subscribe, mounted.actions.getVersion, mounted.actions.getVersion);
|
|
12
|
+
return (_jsxs(_Fragment, { children: [display.gaps.map(({ row }) => {
|
|
13
|
+
const container = mounted.rows.container(`gap-${row.id}`);
|
|
14
|
+
if (container === undefined) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
|
|
18
|
+
return createPortal(_jsx("div", { className: "adecore-editor-diff-gap", children: row.unfoldable ? (_jsx(Button, { size: "xs", disabled: blocked?.has(row.id), title: blocked?.has(row.id) ? t('diff.contextLimit') : undefined, onClick: () => unfold(row.id), children: text })) : (_jsx("span", { children: text })) }), container, `gap-${row.id}`);
|
|
19
|
+
}), display.rows.map((row, index) => {
|
|
20
|
+
if (row.noNewline === undefined) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const container = mounted.actions.container(`no-newline-${index}`);
|
|
24
|
+
return container === undefined
|
|
25
|
+
? null
|
|
26
|
+
: createPortal(_jsx("span", { className: "adecore-editor-diff-no-newline", children: t('diff.noNewline') }), container, `no-newline-${index}`);
|
|
27
|
+
})] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LineToken } from '@adecore/editor';
|
|
2
|
+
import type { DiffSyntaxDocument } from '@adecore/editor/shiki-worker';
|
|
3
|
+
import type { DiffDocument } from '@adecore/merge';
|
|
4
|
+
export type DiffSourceSide = 'old' | 'new';
|
|
5
|
+
export interface DiffSourceTokens {
|
|
6
|
+
get(side: DiffSourceSide, index: number): readonly LineToken[] | null | undefined;
|
|
7
|
+
subscribe(listener: () => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface DiffSyntaxSources {
|
|
10
|
+
readonly documents: Readonly<Record<DiffSourceSide, DiffSyntaxDocument>>;
|
|
11
|
+
locate(side: DiffSourceSide, index: number): {
|
|
12
|
+
readonly block: number;
|
|
13
|
+
readonly line: number;
|
|
14
|
+
} | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function diffSyntaxDocument(document: DiffDocument, language: string, theme: string): DiffSyntaxSources;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function canTokenize(source) {
|
|
2
|
+
if (source.length > 20000) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
let characters = Math.max(0, source.length - 1);
|
|
6
|
+
for (const line of source) {
|
|
7
|
+
characters += line.length;
|
|
8
|
+
if (line.length > 10000 || characters > 2000000) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
export function diffSyntaxDocument(document, language, theme) {
|
|
15
|
+
const locations = { old: [], new: [] };
|
|
16
|
+
const build = (side) => {
|
|
17
|
+
const source = document.sides[side];
|
|
18
|
+
// Skipping a long hidden line loses grammar state; keep the entire side plain instead.
|
|
19
|
+
if (!canTokenize(source)) {
|
|
20
|
+
return { language, theme, blocks: [], resets: [] };
|
|
21
|
+
}
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const resets = [];
|
|
24
|
+
const ranges = document.snapshots
|
|
25
|
+
? [{ from: 0, to: source.length }]
|
|
26
|
+
: document.offsets.map((offset, index) => ({ from: offset[side], to: document.offsets[index + 1]?.[side] ?? source.length }));
|
|
27
|
+
for (const range of ranges) {
|
|
28
|
+
if (!document.snapshots && range.from < range.to) {
|
|
29
|
+
resets.push(blocks.length);
|
|
30
|
+
}
|
|
31
|
+
for (let from = range.from; from < range.to; from += 200) {
|
|
32
|
+
const to = Math.min(from + 200, range.to);
|
|
33
|
+
const block = blocks.length;
|
|
34
|
+
blocks.push(source.slice(from, to).join('\n'));
|
|
35
|
+
for (let index = from; index < to; index++) {
|
|
36
|
+
locations[side][index] = { block, line: index - from };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { language, theme, blocks, resets };
|
|
41
|
+
};
|
|
42
|
+
const documents = { old: build('old'), new: build('new') };
|
|
43
|
+
return {
|
|
44
|
+
documents,
|
|
45
|
+
locate(side, index) {
|
|
46
|
+
return Number.isInteger(index) && index >= 0 ? locations[side][index] : undefined;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { diffRows, wordChanges } from '@adecore/merge';
|
|
2
|
+
export function wordMarks(document) {
|
|
3
|
+
const marks = { old: new Map(), new: new Map() };
|
|
4
|
+
let pairs = 0;
|
|
5
|
+
let characters = 0;
|
|
6
|
+
for (const row of diffRows(document, 'split')) {
|
|
7
|
+
if (row.kind !== 'line' || row.change !== 'pair' || document.hunks[row.hunk].coarse) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const old = document.sides.old[row.old.index];
|
|
11
|
+
const fresh = document.sides.new[row.new.index];
|
|
12
|
+
characters += old.length + fresh.length;
|
|
13
|
+
// Per-pair limits alone still allow thousands of expensive word diffs during one mount.
|
|
14
|
+
if (++pairs > 1000 || characters > 100000) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
const words = wordChanges(old, fresh);
|
|
18
|
+
if (words === null) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const ranges = (text, ranges) => ranges.filter((range) => !document.ignoreWhitespace || /\S/.test(text.slice(range.from, range.to))).map((range) => [range.from, range.to]);
|
|
22
|
+
marks.old.set(row.old.index, ranges(old, words.old));
|
|
23
|
+
marks.new.set(row.new.index, ranges(fresh, words.new));
|
|
24
|
+
}
|
|
25
|
+
return marks;
|
|
26
|
+
}
|