@adecore/editor-react 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallHierarchyPanel.js +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/conflict-model.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function planConflict(base: string, ours: string, theirs: string): Confli
|
|
|
31
31
|
let oursAt = 0;
|
|
32
32
|
let theirsAt = 0;
|
|
33
33
|
let mergedAt = 0;
|
|
34
|
+
|
|
34
35
|
for (const block of blocks) {
|
|
35
36
|
if (block.kind === 'theirs') {
|
|
36
37
|
merges.push({ from: oursAt, to: oursAt + block.ours.length, lines: block.theirs });
|
|
@@ -47,10 +48,13 @@ export function planConflict(base: string, ours: string, theirs: string): Confli
|
|
|
47
48
|
theirsEnd: theirsAt + block.theirs.length
|
|
48
49
|
});
|
|
49
50
|
}
|
|
51
|
+
|
|
50
52
|
mergedAt += block.ours.length;
|
|
51
53
|
}
|
|
54
|
+
|
|
52
55
|
oursAt += block.ours.length;
|
|
53
56
|
theirsAt += block.theirs.length;
|
|
54
57
|
}
|
|
58
|
+
|
|
55
59
|
return { merges, conflicts };
|
|
56
60
|
}
|
package/src/context-menu-rows.ts
CHANGED
|
@@ -36,5 +36,6 @@ export interface MenuGroups {
|
|
|
36
36
|
/* The language rows of the context menu in their groups, each holding only the rows a server answers. */
|
|
37
37
|
export function menuGroupsOf(supports: (method: string) => boolean): MenuGroups {
|
|
38
38
|
const supported = (rows: readonly MenuRowSpec[]): MenuRowSpec[] => rows.filter((row) => supports(row.method));
|
|
39
|
+
|
|
39
40
|
return { goTo: supported(GO_TO), references: supported(REFERENCES), changes: supported(CHANGES) };
|
|
40
41
|
}
|
package/src/context-menu.ts
CHANGED
|
@@ -12,13 +12,16 @@ export class ContextMenuFeature {
|
|
|
12
12
|
|
|
13
13
|
constructor(language: EditorLanguage) {
|
|
14
14
|
this.language = language;
|
|
15
|
+
|
|
15
16
|
const off = language.editor.onContextMenu((menu) => {
|
|
16
17
|
// A right click outside the selection moves the caret first, so the menu is about what was clicked.
|
|
17
18
|
if (!menu.inSelection) {
|
|
18
19
|
language.editor.setCaret(menu.position);
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
this.open(menu.x, menu.y);
|
|
21
23
|
});
|
|
24
|
+
|
|
22
25
|
language.onDispose(() => {
|
|
23
26
|
off();
|
|
24
27
|
this.close();
|
|
@@ -27,6 +30,7 @@ export class ContextMenuFeature {
|
|
|
27
30
|
|
|
28
31
|
close(): void {
|
|
29
32
|
this.request++;
|
|
33
|
+
|
|
30
34
|
if (this.language.popups.getState().menu !== null) {
|
|
31
35
|
this.language.popups.setState({ menu: null });
|
|
32
36
|
}
|
|
@@ -37,6 +41,7 @@ export class ContextMenuFeature {
|
|
|
37
41
|
const { editor } = this.language;
|
|
38
42
|
editor.focus();
|
|
39
43
|
const done = document.execCommand(action);
|
|
44
|
+
|
|
40
45
|
if (!done && action === 'paste') {
|
|
41
46
|
void navigator.clipboard
|
|
42
47
|
?.readText()
|
|
@@ -52,8 +57,10 @@ export class ContextMenuFeature {
|
|
|
52
57
|
const request = ++this.request;
|
|
53
58
|
const { popups, codeActions } = this.language;
|
|
54
59
|
popups.setState({ menu: { x, y, refactors: [] } });
|
|
60
|
+
|
|
55
61
|
void codeActions.list(['refactor']).then((entries) => {
|
|
56
62
|
const shown = popups.getState().menu;
|
|
63
|
+
|
|
57
64
|
if (request === this.request && shown !== null && entries !== null) {
|
|
58
65
|
popups.setState({ menu: { ...shown, refactors: entries } });
|
|
59
66
|
}
|
package/src/definition-link.ts
CHANGED
|
@@ -5,6 +5,33 @@ import { locationsOf } from './hover-content.ts';
|
|
|
5
5
|
import { lineTextOf } from './line-text.ts';
|
|
6
6
|
import { wordRangeAt } from './rename-model.ts';
|
|
7
7
|
|
|
8
|
+
/* Reports the Mod key going down and up, from the window since the editor only says where the pointer is. */
|
|
9
|
+
export function followModKey(apple: boolean, onHeld: (held: boolean) => void): () => void {
|
|
10
|
+
if (typeof window === 'undefined') {
|
|
11
|
+
return () => {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const modKey = apple ? 'Meta' : 'Control';
|
|
15
|
+
|
|
16
|
+
const keys = (event: KeyboardEvent): void => {
|
|
17
|
+
if (event.key === modKey) {
|
|
18
|
+
onHeld(event.type === 'keydown');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const lost = (): void => onHeld(false);
|
|
23
|
+
|
|
24
|
+
window.addEventListener('keydown', keys);
|
|
25
|
+
window.addEventListener('keyup', keys);
|
|
26
|
+
window.addEventListener('blur', lost);
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener('keydown', keys);
|
|
30
|
+
window.removeEventListener('keyup', keys);
|
|
31
|
+
window.removeEventListener('blur', lost);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
/*
|
|
9
36
|
* The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
|
|
10
37
|
* is defined, which is what a Mod+click on it follows. The editor only says where the pointer is, so the
|
|
@@ -22,35 +49,22 @@ export class DefinitionLinkFeature {
|
|
|
22
49
|
constructor(language: EditorLanguage) {
|
|
23
50
|
this.language = language;
|
|
24
51
|
const { editor } = language;
|
|
52
|
+
|
|
25
53
|
const offs = [
|
|
26
54
|
editor.onHover((hover) => {
|
|
27
55
|
this.pointer = hover?.position ?? null;
|
|
28
56
|
this.update();
|
|
29
57
|
}),
|
|
30
58
|
editor.onTextChange(() => this.clear()),
|
|
31
|
-
editor.onViewChange(() => this.clear())
|
|
59
|
+
editor.onViewChange(() => this.clear()),
|
|
60
|
+
followModKey(language.apple, (held) => this.setModHeld(held))
|
|
32
61
|
];
|
|
33
|
-
|
|
34
|
-
const modKey = language.apple ? 'Meta' : 'Control';
|
|
35
|
-
const keys = (event: KeyboardEvent): void => {
|
|
36
|
-
if (event.key === modKey) {
|
|
37
|
-
this.setModHeld(event.type === 'keydown');
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const lost = (): void => this.setModHeld(false);
|
|
41
|
-
window.addEventListener('keydown', keys);
|
|
42
|
-
window.addEventListener('keyup', keys);
|
|
43
|
-
window.addEventListener('blur', lost);
|
|
44
|
-
offs.push(() => {
|
|
45
|
-
window.removeEventListener('keydown', keys);
|
|
46
|
-
window.removeEventListener('keyup', keys);
|
|
47
|
-
window.removeEventListener('blur', lost);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
62
|
+
|
|
50
63
|
language.onDispose(() => {
|
|
51
64
|
for (const off of offs) {
|
|
52
65
|
off();
|
|
53
66
|
}
|
|
67
|
+
|
|
54
68
|
this.clear();
|
|
55
69
|
});
|
|
56
70
|
}
|
|
@@ -66,6 +80,7 @@ export class DefinitionLinkFeature {
|
|
|
66
80
|
this.request++;
|
|
67
81
|
this.lookup?.abort();
|
|
68
82
|
this.shownKey = '';
|
|
83
|
+
|
|
69
84
|
if (this.drawn) {
|
|
70
85
|
this.drawn = false;
|
|
71
86
|
this.language.editor.setLink(null);
|
|
@@ -74,26 +89,35 @@ export class DefinitionLinkFeature {
|
|
|
74
89
|
|
|
75
90
|
private update(): void {
|
|
76
91
|
const { pointer } = this;
|
|
92
|
+
|
|
77
93
|
if (!this.held || pointer === null || !this.language.project.service.supports('textDocument/definition', this.language.uri)) {
|
|
78
94
|
this.clear();
|
|
95
|
+
|
|
79
96
|
return;
|
|
80
97
|
}
|
|
98
|
+
|
|
81
99
|
const { editor, project, uri } = this.language;
|
|
82
100
|
const line = lineTextOf(editor, pointer.line);
|
|
83
101
|
const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
|
|
102
|
+
|
|
84
103
|
if (word === null) {
|
|
85
104
|
this.clear();
|
|
105
|
+
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
108
|
+
|
|
88
109
|
const key = `${word.start.line}:${word.start.character}`;
|
|
110
|
+
|
|
89
111
|
if (key === this.shownKey) {
|
|
90
112
|
return;
|
|
91
113
|
}
|
|
114
|
+
|
|
92
115
|
this.clear();
|
|
93
116
|
this.shownKey = key;
|
|
94
117
|
const request = ++this.request;
|
|
95
118
|
const controller = new AbortController();
|
|
96
119
|
this.lookup = controller;
|
|
120
|
+
|
|
97
121
|
project.service
|
|
98
122
|
.definition(uri, pointer, { signal: controller.signal })
|
|
99
123
|
.then((result) => {
|
package/src/diagnostics-model.ts
CHANGED
|
@@ -12,6 +12,7 @@ const SEVERITIES: Record<number, EditorMarkerSeverity> = { 1: 'error', 2: 'warni
|
|
|
12
12
|
const HINT = 4;
|
|
13
13
|
const UNNECESSARY = 1;
|
|
14
14
|
const DEPRECATED = 2;
|
|
15
|
+
|
|
15
16
|
/* The scroll track's tip is a line or two, so a long message is cut where it stops being a tip. */
|
|
16
17
|
const TICK_MESSAGE_LENGTH = 300;
|
|
17
18
|
|
|
@@ -30,9 +31,9 @@ export function markerOf(diagnostic: Diagnostic): EditorMarker {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/* A diagnostic that names no severity counts as an error. */
|
|
34
34
|
export function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts {
|
|
35
35
|
const count = (severity: number): number => diagnostics.filter((diagnostic) => (diagnostic.severity ?? 1) === severity).length;
|
|
36
|
+
|
|
36
37
|
return { error: count(1), warning: count(2), info: count(3) };
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -40,9 +41,14 @@ export function comparePositions(left: EditorPosition, right: EditorPosition): n
|
|
|
40
41
|
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
export function sameRange(left: EditorRange, right: EditorRange): boolean {
|
|
45
|
+
return comparePositions(left.start, right.start) === 0 && comparePositions(left.end, right.end) === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
/* Where an insertion of `text` at `start` ends, counting a line break of any kind once. */
|
|
44
49
|
export function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
|
|
45
50
|
const lines = text.split(/\r\n|\r|\n/);
|
|
51
|
+
|
|
46
52
|
return lines.length === 1
|
|
47
53
|
? { line: start.line, character: start.character + text.length }
|
|
48
54
|
: { line: start.line + lines.length - 1, character: lines[lines.length - 1]!.length };
|
|
@@ -51,13 +57,17 @@ export function endOfInsertion(start: EditorPosition, text: string): EditorPosit
|
|
|
51
57
|
/* Where a position is once one change is applied to the text it was in. One inside the replaced range lands after the new text. */
|
|
52
58
|
export function shiftPosition(position: EditorPosition, change: EditorContentChange): EditorPosition {
|
|
53
59
|
const { start, end } = change.range;
|
|
60
|
+
|
|
54
61
|
if (comparePositions(position, start) < 0) {
|
|
55
62
|
return position;
|
|
56
63
|
}
|
|
64
|
+
|
|
57
65
|
const inserted = endOfInsertion(start, change.text);
|
|
66
|
+
|
|
58
67
|
if (comparePositions(position, end) < 0) {
|
|
59
68
|
return inserted;
|
|
60
69
|
}
|
|
70
|
+
|
|
61
71
|
return position.line === end.line
|
|
62
72
|
? { line: inserted.line, character: inserted.character + (position.character - end.character) }
|
|
63
73
|
: { line: position.line + (inserted.line - end.line), character: position.character };
|
|
@@ -66,10 +76,12 @@ export function shiftPosition(position: EditorPosition, change: EditorContentCha
|
|
|
66
76
|
export function shiftRange(range: EditorRange, changes: readonly EditorContentChange[]): EditorRange {
|
|
67
77
|
let start = range.start;
|
|
68
78
|
let end = range.end;
|
|
79
|
+
|
|
69
80
|
for (const change of changes) {
|
|
70
81
|
start = shiftPosition(start, change);
|
|
71
82
|
end = shiftPosition(end, change);
|
|
72
83
|
}
|
|
84
|
+
|
|
73
85
|
return { start, end };
|
|
74
86
|
}
|
|
75
87
|
|
|
@@ -81,6 +93,7 @@ export function rangeHolds(range: EditorRange, position: EditorPosition): boolea
|
|
|
81
93
|
/* The problems under a position, worst first. A hint is not a problem, so it is left to the colors and the faded text. */
|
|
82
94
|
export function problemsAt(problems: readonly Problem[], position: EditorPosition): Problem[] {
|
|
83
95
|
const rank = (problem: Problem): number => problem.diagnostic.severity ?? 1;
|
|
96
|
+
|
|
84
97
|
return problems
|
|
85
98
|
.filter((problem) => (problem.diagnostic.severity ?? 1) < HINT && rangeHolds(problem.diagnostic.range, position))
|
|
86
99
|
.sort((left, right) => rank(left) - rank(right));
|
|
@@ -91,20 +104,25 @@ export function neighborProblem(problems: readonly Problem[], from: EditorPositi
|
|
|
91
104
|
const candidates = problems
|
|
92
105
|
.filter((problem) => (problem.diagnostic.severity ?? 1) < HINT)
|
|
93
106
|
.sort((left, right) => comparePositions(left.diagnostic.range.start, right.diagnostic.range.start));
|
|
107
|
+
|
|
94
108
|
if (candidates.length === 0) {
|
|
95
109
|
return null;
|
|
96
110
|
}
|
|
111
|
+
|
|
97
112
|
if (direction === 1) {
|
|
98
113
|
return candidates.find((problem) => comparePositions(problem.diagnostic.range.start, from) > 0) ?? candidates[0]!;
|
|
99
114
|
}
|
|
115
|
+
|
|
100
116
|
return [...candidates].reverse().find((problem) => comparePositions(problem.diagnostic.range.start, from) < 0) ?? candidates[candidates.length - 1]!;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
/* `ts(2551)`, or the code alone, or the source alone, whichever the server gave. */
|
|
104
120
|
export function codeLabelOf(diagnostic: Diagnostic): string {
|
|
105
121
|
const code = diagnostic.code === undefined ? '' : String(diagnostic.code);
|
|
122
|
+
|
|
106
123
|
if (diagnostic.source !== undefined && code !== '') {
|
|
107
124
|
return `${diagnostic.source}(${code})`;
|
|
108
125
|
}
|
|
126
|
+
|
|
109
127
|
return code !== '' ? code : (diagnostic.source ?? '');
|
|
110
128
|
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,9 +2,18 @@ import type { Diagnostic } from '@adecore/lsp';
|
|
|
2
2
|
import { KEY_PRIORITY } from './key-priority.ts';
|
|
3
3
|
import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
|
|
4
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
-
import { sameRange } from './hover-content.ts';
|
|
6
5
|
import { isShortcut } from './shortcut-keys.ts';
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
comparePositions,
|
|
8
|
+
markerOf,
|
|
9
|
+
neighborProblem,
|
|
10
|
+
problemCountsOf,
|
|
11
|
+
problemsAt,
|
|
12
|
+
sameRange,
|
|
13
|
+
severityOf,
|
|
14
|
+
shiftRange,
|
|
15
|
+
type Problem
|
|
16
|
+
} from './diagnostics-model.ts';
|
|
8
17
|
|
|
9
18
|
export interface ProblemCounts {
|
|
10
19
|
readonly error: number;
|
|
@@ -26,22 +35,29 @@ export class DiagnosticsFeature {
|
|
|
26
35
|
constructor(language: EditorLanguage) {
|
|
27
36
|
this.language = language;
|
|
28
37
|
const { editor, project, uri } = language;
|
|
38
|
+
|
|
29
39
|
const reports = project.service.onDiagnostics((report) => {
|
|
30
40
|
if (report.uri === uri) {
|
|
31
41
|
this.reports.set(report.source, report.diagnostics);
|
|
32
42
|
this.rebuild();
|
|
33
43
|
}
|
|
34
44
|
});
|
|
45
|
+
|
|
35
46
|
const edits = editor.onTextChange((change) => this.shift(change.changes));
|
|
47
|
+
|
|
36
48
|
// These keys are the editor's while it has the keyboard; elsewhere the menu's commands answer.
|
|
37
49
|
const keys = language.onKey((event) => {
|
|
38
50
|
const next = isShortcut(this.language.shortcuts.nextProblem, event);
|
|
51
|
+
|
|
39
52
|
if (!next && !isShortcut(this.language.shortcuts.previousProblem, event)) {
|
|
40
53
|
return false;
|
|
41
54
|
}
|
|
55
|
+
|
|
42
56
|
this.step(next ? 1 : -1);
|
|
57
|
+
|
|
43
58
|
return true;
|
|
44
59
|
}, KEY_PRIORITY.diagnostics);
|
|
60
|
+
|
|
45
61
|
language.onDispose(() => {
|
|
46
62
|
keys();
|
|
47
63
|
reports.dispose();
|
|
@@ -67,26 +83,33 @@ export class DiagnosticsFeature {
|
|
|
67
83
|
step(direction: 1 | -1): boolean {
|
|
68
84
|
const caret = this.language.editor.getCaret();
|
|
69
85
|
const target = neighborProblem(this.list, caret, direction);
|
|
86
|
+
|
|
70
87
|
if (target === null) {
|
|
71
88
|
return false;
|
|
72
89
|
}
|
|
90
|
+
|
|
73
91
|
const { start } = target.diagnostic.range;
|
|
74
92
|
this.reveal(target, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
|
|
93
|
+
|
|
75
94
|
return true;
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
/* Puts the caret on the first problem of the file. */
|
|
79
98
|
goToFirst(): boolean {
|
|
80
99
|
const target = neighborProblem(this.list, { line: -1, character: 0 }, 1);
|
|
100
|
+
|
|
81
101
|
if (target === null) {
|
|
82
102
|
return false;
|
|
83
103
|
}
|
|
104
|
+
|
|
84
105
|
this.reveal(target, 'center');
|
|
106
|
+
|
|
85
107
|
return true;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
onChange(listener: () => void): () => void {
|
|
89
111
|
this.listeners.add(listener);
|
|
112
|
+
|
|
90
113
|
return () => {
|
|
91
114
|
this.listeners.delete(listener);
|
|
92
115
|
};
|
|
@@ -104,20 +127,25 @@ export class DiagnosticsFeature {
|
|
|
104
127
|
/* The problems follow the edit as the markers do. Their number does not change, so nobody is told. */
|
|
105
128
|
private shift(changes: readonly EditorContentChange[]): void {
|
|
106
129
|
let moved = false;
|
|
130
|
+
|
|
107
131
|
for (const [server, diagnostics] of this.reports) {
|
|
108
132
|
let shifted: Diagnostic[] | null = null;
|
|
133
|
+
|
|
109
134
|
for (const [index, diagnostic] of diagnostics.entries()) {
|
|
110
135
|
const range = shiftRange(diagnostic.range, changes);
|
|
136
|
+
|
|
111
137
|
if (!sameRange(range, diagnostic.range)) {
|
|
112
138
|
shifted ??= [...diagnostics];
|
|
113
139
|
shifted[index] = { ...diagnostic, range };
|
|
114
140
|
}
|
|
115
141
|
}
|
|
142
|
+
|
|
116
143
|
if (shifted !== null) {
|
|
117
144
|
this.reports.set(server, shifted);
|
|
118
145
|
moved = true;
|
|
119
146
|
}
|
|
120
147
|
}
|
|
148
|
+
|
|
121
149
|
if (moved) {
|
|
122
150
|
this.collect();
|
|
123
151
|
}
|
|
@@ -126,6 +154,7 @@ export class DiagnosticsFeature {
|
|
|
126
154
|
private rebuild(): void {
|
|
127
155
|
this.collect();
|
|
128
156
|
this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
|
|
157
|
+
|
|
129
158
|
for (const listener of [...this.listeners]) {
|
|
130
159
|
listener();
|
|
131
160
|
}
|
package/src/diff-expansion.ts
CHANGED
|
@@ -11,39 +11,48 @@ export const DEFAULT_UNFOLD_LIMITS: DiffLimits = { rows: 20000, characters: 2000
|
|
|
11
11
|
export function fitsDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, limits: DiffLimits, maxRows = limits.rows): boolean {
|
|
12
12
|
let rows = 0;
|
|
13
13
|
let characters = 0;
|
|
14
|
+
|
|
14
15
|
for (const row of diffRows(document, 'unified', unfolded)) {
|
|
15
16
|
if (row.kind === 'gap') {
|
|
16
17
|
continue;
|
|
17
18
|
}
|
|
19
|
+
|
|
18
20
|
const text = row.new === undefined ? document.sides.old[row.old!.index]! : document.sides.new[row.new.index]!;
|
|
19
21
|
characters += text.length + (rows > 0 ? 1 : 0);
|
|
22
|
+
|
|
20
23
|
if (++rows > maxRows || characters > limits.characters || text.length > limits.lineLength) {
|
|
21
24
|
return false;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
27
|
+
|
|
24
28
|
return true;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function expandDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, id: number, limits: DiffLimits): ReadonlyMap<number, number> {
|
|
28
32
|
const rows = diffRows(document, 'unified', unfolded);
|
|
29
33
|
const gap = rows.find((row) => row.kind === 'gap' && row.id === id);
|
|
34
|
+
|
|
30
35
|
if (gap?.kind !== 'gap' || !gap.unfoldable) {
|
|
31
36
|
return unfolded;
|
|
32
37
|
}
|
|
38
|
+
|
|
33
39
|
const visible = rows.filter((row) => row.kind === 'line').length;
|
|
34
40
|
const maxRows = Math.min(limits.rows, visible + 500);
|
|
35
41
|
const previous = unfolded.get(id) ?? 0;
|
|
36
42
|
let low = 0;
|
|
37
43
|
let high = Math.min(500, gap.oldCount);
|
|
44
|
+
|
|
38
45
|
// Between-hunk gaps reveal from both ends; search the per-end count within the shared row budget.
|
|
39
46
|
while (low < high) {
|
|
40
47
|
const count = Math.ceil((low + high) / 2);
|
|
41
48
|
const candidate = new Map(unfolded).set(id, previous + count);
|
|
49
|
+
|
|
42
50
|
if (fitsDiff(document, candidate, limits, maxRows)) {
|
|
43
51
|
low = count;
|
|
44
52
|
} else {
|
|
45
53
|
high = count - 1;
|
|
46
54
|
}
|
|
47
55
|
}
|
|
56
|
+
|
|
48
57
|
return low === 0 ? unfolded : new Map(unfolded).set(id, previous + low);
|
|
49
58
|
}
|
package/src/diff-overlays.tsx
CHANGED
|
@@ -26,18 +26,24 @@ export function DiffOverlays({
|
|
|
26
26
|
blocked?: ReadonlySet<number>;
|
|
27
27
|
}) {
|
|
28
28
|
const { t } = useTranslation('editor');
|
|
29
|
+
|
|
29
30
|
const getRows = () => mounted.rows.getContainers();
|
|
30
31
|
const getActions = () => mounted.actions.getContainers();
|
|
32
|
+
|
|
31
33
|
const rows = useSyncExternalStore((listener) => mounted.rows.subscribe(listener), getRows, getRows);
|
|
32
34
|
const actions = useSyncExternalStore((listener) => mounted.actions.subscribe(listener), getActions, getActions);
|
|
35
|
+
|
|
33
36
|
return (
|
|
34
37
|
<>
|
|
35
38
|
{display.gaps.map(({ row }) => {
|
|
36
39
|
const container = rows.get(`gap-${row.id}`);
|
|
40
|
+
|
|
37
41
|
if (container === undefined) {
|
|
38
42
|
return null;
|
|
39
43
|
}
|
|
44
|
+
|
|
40
45
|
const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
|
|
46
|
+
|
|
41
47
|
return createPortal(
|
|
42
48
|
<div className="adecore-editor-diff-gap">
|
|
43
49
|
{row.unfoldable && blocked?.has(row.id) === true ? (
|
|
@@ -65,7 +71,9 @@ export function DiffOverlays({
|
|
|
65
71
|
if (row.noNewline === undefined) {
|
|
66
72
|
return null;
|
|
67
73
|
}
|
|
74
|
+
|
|
68
75
|
const container = actions.get(`no-newline-${index}`);
|
|
76
|
+
|
|
69
77
|
return container === undefined
|
|
70
78
|
? null
|
|
71
79
|
: createPortal(<span className="adecore-editor-diff-no-newline">{t('diff.noNewline')}</span>, container, `no-newline-${index}`);
|
package/src/diff-syntax.ts
CHANGED
|
@@ -15,49 +15,103 @@ export interface DiffSyntaxSources {
|
|
|
15
15
|
locate(side: DiffSourceSide, index: number): { readonly block: number; readonly line: number } | undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* Calls `refresh` once per frame while tokens arrive, or in a microtask where the window draws no frames. */
|
|
19
|
+
export function repaintOnTokens(ownerWindow: Window | null | undefined, tokens: DiffSourceTokens | undefined, refresh: () => void): () => void {
|
|
20
|
+
let pending = false;
|
|
21
|
+
let stopped = false;
|
|
22
|
+
let frame: number | undefined;
|
|
23
|
+
|
|
24
|
+
const schedule = (): void => {
|
|
25
|
+
if (pending) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pending = true;
|
|
30
|
+
|
|
31
|
+
const paint = (): void => {
|
|
32
|
+
pending = false;
|
|
33
|
+
|
|
34
|
+
if (!stopped) {
|
|
35
|
+
refresh();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (ownerWindow?.requestAnimationFrame !== undefined) {
|
|
40
|
+
frame = ownerWindow.requestAnimationFrame(paint);
|
|
41
|
+
} else {
|
|
42
|
+
queueMicrotask(paint);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const stop = tokens?.subscribe(schedule);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
stopped = true;
|
|
50
|
+
stop?.();
|
|
51
|
+
|
|
52
|
+
if (frame !== undefined) {
|
|
53
|
+
ownerWindow?.cancelAnimationFrame?.(frame);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
18
58
|
function canTokenize(source: readonly string[]): boolean {
|
|
19
59
|
if (source.length > 20000) {
|
|
20
60
|
return false;
|
|
21
61
|
}
|
|
62
|
+
|
|
22
63
|
let characters = Math.max(0, source.length - 1);
|
|
64
|
+
|
|
23
65
|
for (const line of source) {
|
|
24
66
|
characters += line.length;
|
|
67
|
+
|
|
25
68
|
if (line.length > 10000 || characters > 2000000) {
|
|
26
69
|
return false;
|
|
27
70
|
}
|
|
28
71
|
}
|
|
72
|
+
|
|
29
73
|
return true;
|
|
30
74
|
}
|
|
31
75
|
|
|
32
76
|
export function diffSyntaxDocument(document: DiffDocument, language: string, theme: string): DiffSyntaxSources {
|
|
33
77
|
const locations: Record<DiffSourceSide, Array<{ block: number; line: number }>> = { old: [], new: [] };
|
|
78
|
+
|
|
34
79
|
const build = (side: DiffSourceSide): DiffSyntaxDocument => {
|
|
35
80
|
const source = document.sides[side];
|
|
81
|
+
|
|
36
82
|
// Skipping a long hidden line loses grammar state; keep the entire side plain instead.
|
|
37
83
|
if (!canTokenize(source)) {
|
|
38
84
|
return { language, theme, blocks: [], resets: [] };
|
|
39
85
|
}
|
|
86
|
+
|
|
40
87
|
const blocks: string[] = [];
|
|
41
88
|
const resets: number[] = [];
|
|
89
|
+
|
|
42
90
|
const ranges = document.snapshots
|
|
43
91
|
? [{ from: 0, to: source.length }]
|
|
44
92
|
: document.offsets.map((offset, index) => ({ from: offset[side], to: document.offsets[index + 1]?.[side] ?? source.length }));
|
|
93
|
+
|
|
45
94
|
for (const range of ranges) {
|
|
46
95
|
if (!document.snapshots && range.from < range.to) {
|
|
47
96
|
resets.push(blocks.length);
|
|
48
97
|
}
|
|
98
|
+
|
|
49
99
|
for (let from = range.from; from < range.to; from += 200) {
|
|
50
100
|
const to = Math.min(from + 200, range.to);
|
|
51
101
|
const block = blocks.length;
|
|
52
102
|
blocks.push(source.slice(from, to).join('\n'));
|
|
103
|
+
|
|
53
104
|
for (let index = from; index < to; index++) {
|
|
54
105
|
locations[side][index] = { block, line: index - from };
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
}
|
|
109
|
+
|
|
58
110
|
return { language, theme, blocks, resets };
|
|
59
111
|
};
|
|
112
|
+
|
|
60
113
|
const documents = { old: build('old'), new: build('new') };
|
|
114
|
+
|
|
61
115
|
return {
|
|
62
116
|
documents,
|
|
63
117
|
locate(side, index) {
|
package/src/diff-wordmarks.ts
CHANGED
|
@@ -7,25 +7,33 @@ export function wordMarks(document: DiffDocument): {
|
|
|
7
7
|
const marks = { old: new Map<number, readonly (readonly [number, number])[]>(), new: new Map<number, readonly (readonly [number, number])[]>() };
|
|
8
8
|
let pairs = 0;
|
|
9
9
|
let characters = 0;
|
|
10
|
+
|
|
10
11
|
for (const row of diffRows(document, 'split')) {
|
|
11
12
|
if (row.kind !== 'line' || row.change !== 'pair' || document.hunks[row.hunk!]!.coarse) {
|
|
12
13
|
continue;
|
|
13
14
|
}
|
|
15
|
+
|
|
14
16
|
const old = document.sides.old[row.old!.index]!;
|
|
15
17
|
const fresh = document.sides.new[row.new!.index]!;
|
|
16
18
|
characters += old.length + fresh.length;
|
|
19
|
+
|
|
17
20
|
// Per-pair limits alone still allow thousands of expensive word diffs during one mount.
|
|
18
21
|
if (++pairs > 1000 || characters > 100000) {
|
|
19
22
|
break;
|
|
20
23
|
}
|
|
24
|
+
|
|
21
25
|
const words = wordChanges(old, fresh);
|
|
26
|
+
|
|
22
27
|
if (words === null) {
|
|
23
28
|
continue;
|
|
24
29
|
}
|
|
30
|
+
|
|
25
31
|
const ranges = (text: string, ranges: readonly DiffWordRange[]): Array<readonly [number, number]> =>
|
|
26
32
|
ranges.filter((range) => !document.ignoreWhitespace || /\S/.test(text.slice(range.from, range.to))).map((range) => [range.from, range.to]);
|
|
33
|
+
|
|
27
34
|
marks.old.set(row.old!.index, ranges(old, words.old));
|
|
28
35
|
marks.new.set(row.new!.index, ranges(fresh, words.new));
|
|
29
36
|
}
|
|
37
|
+
|
|
30
38
|
return marks;
|
|
31
39
|
}
|