@adecore/editor-react 0.21.1 → 0.21.2
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/AnchoredPopup.js +2 -3
- package/dist/HoverSections.js +5 -11
- package/dist/PeekPanel.js +6 -13
- package/dist/RenameCard.js +2 -2
- package/dist/SymbolPicker.js +2 -3
- package/dist/code-actions-model.js +9 -8
- package/dist/code-actions.d.ts +1 -1
- package/dist/code-actions.js +21 -20
- package/dist/code-authors.js +1 -1
- package/dist/code-vision.d.ts +1 -0
- package/dist/code-vision.js +17 -14
- package/dist/completion-call.js +2 -6
- package/dist/completion-model.d.ts +3 -2
- package/dist/completion-model.js +8 -9
- package/dist/completion.js +8 -10
- package/dist/context-menu.js +4 -5
- package/dist/definition-link.js +2 -1
- package/dist/diagnostics-model.d.ts +2 -0
- package/dist/diagnostics-model.js +8 -2
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +13 -12
- package/dist/editor-language.d.ts +2 -2
- package/dist/editor-language.js +10 -10
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +3 -0
- package/dist/folding.js +5 -24
- package/dist/history.d.ts +1 -0
- package/dist/history.js +9 -12
- package/dist/hover-content.d.ts +1 -0
- package/dist/hover-content.js +5 -4
- package/dist/hover.d.ts +2 -2
- package/dist/hover.js +10 -9
- package/dist/inlay-hints.js +4 -4
- package/dist/language-ids.js +1 -1
- package/dist/line-text.d.ts +3 -0
- package/dist/line-text.js +7 -0
- package/dist/navigation.js +9 -9
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +4 -0
- package/dist/peek.js +11 -4
- package/dist/project-language.js +15 -13
- package/dist/project-problems.d.ts +1 -2
- package/dist/project-problems.js +2 -4
- package/dist/refresher.js +11 -6
- package/dist/rename.d.ts +2 -2
- package/dist/rename.js +14 -12
- package/dist/review-group.d.ts +1 -0
- package/dist/review-group.js +10 -14
- package/dist/row-host.d.ts +13 -20
- package/dist/row-host.js +33 -60
- package/dist/semantic-model.js +4 -6
- package/dist/semantic-tokens.js +2 -4
- package/dist/signature.js +1 -1
- package/dist/snippet-session.js +3 -4
- package/dist/symbol-links.js +2 -2
- package/dist/symbol-picker-model.js +15 -9
- package/dist/symbol-picker.js +2 -1
- package/dist/symbols.d.ts +7 -1
- package/dist/symbols.js +22 -28
- package/dist/testing.js +3 -3
- package/dist/word-diff.js +2 -4
- package/dist/workspace-edit.js +7 -8
- package/package.json +5 -5
- package/src/AnchoredPopup.tsx +2 -3
- package/src/HoverSections.tsx +5 -11
- package/src/PeekPanel.tsx +6 -14
- package/src/RenameCard.tsx +2 -2
- package/src/SymbolPicker.tsx +3 -4
- package/src/code-actions-model.ts +10 -8
- package/src/code-actions.ts +36 -38
- package/src/code-authors.ts +2 -2
- package/src/code-vision.ts +19 -15
- package/src/completion-call.ts +2 -6
- package/src/completion-model.ts +10 -12
- package/src/completion.ts +8 -13
- package/src/context-menu.ts +4 -5
- package/src/definition-link.ts +2 -1
- package/src/diagnostics-model.ts +10 -2
- package/src/diagnostics.ts +15 -13
- package/src/editor-language.ts +11 -11
- package/src/error-message.ts +3 -0
- package/src/folding.ts +6 -26
- package/src/history.ts +11 -12
- package/src/hover-content.ts +6 -4
- package/src/hover.ts +10 -9
- package/src/inlay-hints.ts +4 -4
- package/src/language-ids.ts +1 -1
- package/src/line-text.ts +10 -0
- package/src/navigation.ts +11 -10
- package/src/paths.ts +5 -0
- package/src/peek.ts +11 -10
- package/src/project-language.ts +17 -16
- package/src/project-problems.ts +3 -6
- package/src/refresher.ts +12 -6
- package/src/rename.ts +16 -18
- package/src/review-group.ts +11 -15
- package/src/row-host.ts +54 -76
- package/src/semantic-model.ts +4 -6
- package/src/semantic-tokens.ts +2 -4
- package/src/signature.ts +1 -1
- package/src/snippet-session.ts +3 -4
- package/src/symbol-links.ts +2 -2
- package/src/symbol-picker-model.ts +17 -9
- package/src/symbol-picker.ts +2 -1
- package/src/symbols.ts +29 -28
- package/src/testing.ts +4 -2
- package/src/view-state.ts +2 -0
- package/src/word-diff.ts +2 -4
- package/src/workspace-edit.ts +7 -8
package/src/review-group.ts
CHANGED
|
@@ -4,7 +4,6 @@ export interface ReviewMember {
|
|
|
4
4
|
hide(runIds: readonly string[]): void;
|
|
5
5
|
/* Restores runs after a refused operation. */
|
|
6
6
|
show(runIds: readonly string[]): void;
|
|
7
|
-
/* Refreshes runs after an operation. */
|
|
8
7
|
refresh(): void;
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -24,20 +23,21 @@ export class ReviewGroup {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
hide(runIds: readonly string[]): void {
|
|
27
|
-
|
|
28
|
-
member.hide(runIds);
|
|
29
|
-
}
|
|
26
|
+
this.each((member) => member.hide(runIds));
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
show(runIds: readonly string[]): void {
|
|
33
|
-
|
|
34
|
-
member.show(runIds);
|
|
35
|
-
}
|
|
30
|
+
this.each((member) => member.show(runIds));
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
refresh(): void {
|
|
34
|
+
this.each((member) => member.refresh());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Over a copy, since a member may leave while it hears. */
|
|
38
|
+
private each(action: (member: ReviewMember) => void): void {
|
|
39
39
|
for (const member of [...this.members]) {
|
|
40
|
-
member
|
|
40
|
+
action(member);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -46,13 +46,9 @@ const groups = new Map<string, { group: ReviewGroup; holders: number }>();
|
|
|
46
46
|
|
|
47
47
|
/* The host supplies a shared file key; the group is released with its last editor. */
|
|
48
48
|
export function joinReviewGroup(key: string): { group: ReviewGroup; leave(): void } {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
groups.set(key, entry);
|
|
53
|
-
}
|
|
54
|
-
entry.holders++;
|
|
55
|
-
const held = entry;
|
|
49
|
+
const held = groups.get(key) ?? { group: new ReviewGroup(), holders: 0 };
|
|
50
|
+
groups.set(key, held);
|
|
51
|
+
held.holders++;
|
|
56
52
|
let left = false;
|
|
57
53
|
return {
|
|
58
54
|
group: held.group,
|
package/src/row-host.ts
CHANGED
|
@@ -8,14 +8,16 @@ export interface HostedRow {
|
|
|
8
8
|
height: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
export interface HostedAction {
|
|
12
|
+
id: string;
|
|
13
|
+
/* Zero-based. */
|
|
14
|
+
line: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* The elements an editor made for one owner's entries, and a version React reads them by. */
|
|
18
|
+
abstract class ContainerHost {
|
|
19
|
+
protected readonly editor: Editor;
|
|
20
|
+
protected readonly owner: string;
|
|
19
21
|
private readonly containers = new Map<string, HTMLElement>();
|
|
20
22
|
private readonly listeners = new Set<() => void>();
|
|
21
23
|
private version = 0;
|
|
@@ -39,19 +41,50 @@ export class RowHost {
|
|
|
39
41
|
return this.containers.get(id);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/*
|
|
43
|
-
|
|
44
|
-
const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
|
|
44
|
+
/* False for the same entries again, so an editor does not measure them twice; otherwise drops the elements of entries that left. */
|
|
45
|
+
protected replace(signature: string, ids: readonly string[]): boolean {
|
|
45
46
|
if (signature === this.signature) {
|
|
46
|
-
return;
|
|
47
|
+
return false;
|
|
47
48
|
}
|
|
48
49
|
this.signature = signature;
|
|
49
|
-
const kept = new Set(
|
|
50
|
+
const kept = new Set(ids);
|
|
50
51
|
for (const id of [...this.containers.keys()]) {
|
|
51
52
|
if (!kept.has(id)) {
|
|
52
53
|
this.containers.delete(id);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected mounted(id: string, container: HTMLElement): void {
|
|
60
|
+
this.containers.set(id, container);
|
|
61
|
+
this.bump();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected bump(): void {
|
|
65
|
+
this.version++;
|
|
66
|
+
for (const listener of [...this.listeners]) {
|
|
67
|
+
listener();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
|
|
74
|
+
* into them. The editor makes an element again whenever its row scrolls back into view, which is why a
|
|
75
|
+
* row's content never keeps state of its own.
|
|
76
|
+
*/
|
|
77
|
+
export class RowHost extends ContainerHost {
|
|
78
|
+
set(rows: readonly HostedRow[]): void {
|
|
79
|
+
const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
|
|
80
|
+
if (
|
|
81
|
+
!this.replace(
|
|
82
|
+
signature,
|
|
83
|
+
rows.map((row) => row.id)
|
|
84
|
+
)
|
|
85
|
+
) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
55
88
|
this.editor.setWidgets(
|
|
56
89
|
rows.map((row) => ({
|
|
57
90
|
id: row.id,
|
|
@@ -68,24 +101,6 @@ export class RowHost {
|
|
|
68
101
|
clear(): void {
|
|
69
102
|
this.set([]);
|
|
70
103
|
}
|
|
71
|
-
|
|
72
|
-
private mounted(id: string, container: HTMLElement): void {
|
|
73
|
-
this.containers.set(id, container);
|
|
74
|
-
this.bump();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private bump(): void {
|
|
78
|
-
this.version++;
|
|
79
|
-
for (const listener of [...this.listeners]) {
|
|
80
|
-
listener();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface HostedAction {
|
|
86
|
-
id: string;
|
|
87
|
-
/* Zero-based. */
|
|
88
|
-
line: number;
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
/*
|
|
@@ -93,52 +108,22 @@ export interface HostedAction {
|
|
|
93
108
|
* them. An element lives as long as its action does, unlike a row's, so what React draws into it keeps
|
|
94
109
|
* its state.
|
|
95
110
|
*/
|
|
96
|
-
export class LineActionHost {
|
|
97
|
-
private readonly editor: Editor;
|
|
98
|
-
private readonly owner: string;
|
|
99
|
-
private readonly containers = new Map<string, HTMLElement>();
|
|
100
|
-
private readonly listeners = new Set<() => void>();
|
|
101
|
-
private version = 0;
|
|
102
|
-
private signature = '';
|
|
103
|
-
|
|
104
|
-
constructor(editor: Editor, owner: string) {
|
|
105
|
-
this.editor = editor;
|
|
106
|
-
this.owner = owner;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getVersion = (): number => this.version;
|
|
110
|
-
|
|
111
|
-
subscribe = (listener: () => void): (() => void) => {
|
|
112
|
-
this.listeners.add(listener);
|
|
113
|
-
return () => {
|
|
114
|
-
this.listeners.delete(listener);
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
container(id: string): HTMLElement | undefined {
|
|
119
|
-
return this.containers.get(id);
|
|
120
|
-
}
|
|
121
|
-
|
|
111
|
+
export class LineActionHost extends ContainerHost {
|
|
122
112
|
set(actions: readonly HostedAction[]): void {
|
|
123
113
|
const signature = actions.map((action) => `${action.id}@${action.line}`).join('|');
|
|
124
|
-
if (
|
|
114
|
+
if (
|
|
115
|
+
!this.replace(
|
|
116
|
+
signature,
|
|
117
|
+
actions.map((action) => action.id)
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
125
120
|
return;
|
|
126
121
|
}
|
|
127
|
-
this.signature = signature;
|
|
128
|
-
const kept = new Set(actions.map((action) => action.id));
|
|
129
|
-
for (const id of [...this.containers.keys()]) {
|
|
130
|
-
if (!kept.has(id)) {
|
|
131
|
-
this.containers.delete(id);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
122
|
this.editor.setLineActions(
|
|
135
123
|
actions.map((action) => ({
|
|
136
124
|
id: action.id,
|
|
137
125
|
line: action.line,
|
|
138
|
-
render: (container: HTMLElement) =>
|
|
139
|
-
this.containers.set(action.id, container);
|
|
140
|
-
this.bump();
|
|
141
|
-
}
|
|
126
|
+
render: (container: HTMLElement) => this.mounted(action.id, container)
|
|
142
127
|
})),
|
|
143
128
|
this.owner
|
|
144
129
|
);
|
|
@@ -148,11 +133,4 @@ export class LineActionHost {
|
|
|
148
133
|
clear(): void {
|
|
149
134
|
this.set([]);
|
|
150
135
|
}
|
|
151
|
-
|
|
152
|
-
private bump(): void {
|
|
153
|
-
this.version++;
|
|
154
|
-
for (const listener of [...this.listeners]) {
|
|
155
|
-
listener();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
136
|
}
|
package/src/semantic-model.ts
CHANGED
|
@@ -2,12 +2,10 @@ import type { EditorSemanticToken } from '@adecore/editor';
|
|
|
2
2
|
import type { SemanticTokens, SemanticTokensLegend } from '@adecore/lsp';
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
* The TextMate scopes a classification stands for, so the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* local to a function, so a variable it leaves unmarked lives at the top of a file or comes from a
|
|
10
|
-
* library, which the scheme draws as a global.
|
|
5
|
+
* The TextMate scopes a classification stands for, so the theme colors it as its grammar would; null where
|
|
6
|
+
* the grammar already did better (a keyword, a string, a local variable). Calls, methods, statics and
|
|
7
|
+
* declarations get scopes of their own, as the color scheme tells them apart. `scoped` says the server
|
|
8
|
+
* marks what is local to a function, so a variable it leaves unmarked is drawn as a global.
|
|
11
9
|
*/
|
|
12
10
|
export function scopesOf(type: string, modifiers: ReadonlySet<string>, scoped = false): string[] | null {
|
|
13
11
|
switch (type) {
|
package/src/semantic-tokens.ts
CHANGED
|
@@ -7,10 +7,8 @@ const METHOD = 'textDocument/semanticTokens/full';
|
|
|
7
7
|
const PAUSE_MS = 200;
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* Colors
|
|
11
|
-
*
|
|
12
|
-
* typing and when a server says its offer changed, and keeps drawing the last answer, which follows its
|
|
13
|
-
* text through edits, in between, so the colors never blink out while the next one is on its way.
|
|
10
|
+
* Colors from what the language servers know, over the grammar's guess. It asks after a pause in typing
|
|
11
|
+
* and when a server's offer changed, and keeps drawing the last answer meanwhile so the colors never blink out.
|
|
14
12
|
*/
|
|
15
13
|
export class SemanticTokensFeature {
|
|
16
14
|
private readonly refresher: Refresher;
|
package/src/signature.ts
CHANGED
|
@@ -67,7 +67,7 @@ export class SignatureFeature {
|
|
|
67
67
|
this.ask({ triggerKind: 2, triggerCharacter: '(', isRetrigger: false });
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/* Shows the next
|
|
70
|
+
/* Shows the next or previous overload; false when the card has fewer than two, which leaves the arrow to the caret. */
|
|
71
71
|
private cycle(step: 1 | -1): boolean {
|
|
72
72
|
const { help } = this;
|
|
73
73
|
if (help === null || help.signatures.length < 2 || !this.isOpen) {
|
package/src/snippet-session.ts
CHANGED
|
@@ -12,10 +12,9 @@ export function positionInInsertion(start: EditorPosition, text: string, offset:
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/*
|
|
15
|
-
* The tab stops of a snippet that was just inserted.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* a stop does not touch the others: they follow the text, but a mirror of a stop is not edited along.
|
|
15
|
+
* The tab stops of a snippet that was just inserted. Tab and Shift+Tab move between them; the last stop,
|
|
16
|
+
* Escape, a caret that leaves the snippet and any change that is not typing end it. A mirror of a stop
|
|
17
|
+
* follows the text but is not edited along.
|
|
19
18
|
*/
|
|
20
19
|
export class SnippetFeature {
|
|
21
20
|
private readonly language: EditorLanguage;
|
package/src/symbol-links.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { Location, SymbolInformation, WorkspaceSymbol, WorkspaceSymbolResul
|
|
|
3
3
|
/* The classes a name in a signature has: a pointer and an underline on hover, and the accent only then, so the signature stays calm. */
|
|
4
4
|
export const SYMBOL_LINK_CLASS = 'cursor-pointer hover:text-accent hover:underline';
|
|
5
5
|
|
|
6
|
-
/* A name that reads as a type: it starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
|
|
7
6
|
const TYPE_NAME = /^[A-Z][A-Za-z0-9_]*$/;
|
|
8
7
|
|
|
8
|
+
/* A name that starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
|
|
9
9
|
export function isTypeName(name: string): boolean {
|
|
10
10
|
return TYPE_NAME.test(name) && /[a-z]/.test(name);
|
|
11
11
|
}
|
|
@@ -27,7 +27,7 @@ export function declaredNameOf(code: string): string | null {
|
|
|
27
27
|
return DECLARES.exec(code)?.[1] ?? null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/*
|
|
30
|
+
/* The symbol kinds a type name can mean: module, namespace, class, enum, interface, struct and type parameter. */
|
|
31
31
|
const TYPE_KINDS = new Set([2, 3, 5, 10, 11, 23, 26]);
|
|
32
32
|
|
|
33
33
|
interface Found {
|
|
@@ -40,6 +40,8 @@ export function groupOfKind(kind: number): SymbolGroup {
|
|
|
40
40
|
return GROUP_OF_KIND[kind] ?? 'other';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const ENUM_MEMBER = 22;
|
|
44
|
+
|
|
43
45
|
const LETTERS: Record<SymbolGroup, string> = {
|
|
44
46
|
namespaces: 'N',
|
|
45
47
|
classes: 'C',
|
|
@@ -55,20 +57,26 @@ const LETTERS: Record<SymbolGroup, string> = {
|
|
|
55
57
|
|
|
56
58
|
/* The letter in the badge in front of a symbol. */
|
|
57
59
|
export function letterOfKind(kind: number): string {
|
|
58
|
-
return kind ===
|
|
60
|
+
return kind === ENUM_MEMBER ? 'e' : LETTERS[groupOfKind(kind)];
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export type SymbolTone = 'callable' | 'value' | 'type' | 'other';
|
|
62
64
|
|
|
65
|
+
const TONES: Record<SymbolGroup, SymbolTone> = {
|
|
66
|
+
namespaces: 'other',
|
|
67
|
+
classes: 'type',
|
|
68
|
+
interfaces: 'type',
|
|
69
|
+
enums: 'type',
|
|
70
|
+
functions: 'callable',
|
|
71
|
+
methods: 'callable',
|
|
72
|
+
properties: 'value',
|
|
73
|
+
variables: 'value',
|
|
74
|
+
constants: 'value',
|
|
75
|
+
other: 'other'
|
|
76
|
+
};
|
|
77
|
+
|
|
63
78
|
export function toneOfKind(kind: number): SymbolTone {
|
|
64
|
-
|
|
65
|
-
return group === 'functions' || group === 'methods'
|
|
66
|
-
? 'callable'
|
|
67
|
-
: group === 'classes' || group === 'interfaces' || group === 'enums'
|
|
68
|
-
? 'type'
|
|
69
|
-
: group === 'properties' || group === 'variables' || group === 'constants'
|
|
70
|
-
? 'value'
|
|
71
|
-
: 'other';
|
|
79
|
+
return TONES[groupOfKind(kind)];
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/* One symbol of a file as a row of the picker. */
|
package/src/symbol-picker.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { basenameOf } from './paths.ts';
|
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { entriesOf, workspaceEntriesOf, type SymbolEntry, type WorkspaceEntry } from './symbol-picker-model.ts';
|
|
5
5
|
import { isShortcut } from './shortcut-keys.ts';
|
|
6
|
+
import { messageOf } from './error-message.ts';
|
|
6
7
|
|
|
7
8
|
const TOAST_ID = 'language-symbols';
|
|
8
9
|
|
|
@@ -43,7 +44,7 @@ export class SymbolPickerFeature {
|
|
|
43
44
|
this.language.project.host.notify?.({
|
|
44
45
|
id: TOAST_ID,
|
|
45
46
|
kind: 'error',
|
|
46
|
-
title: this.language.project.i18n.t('editor:language.symbols.failed', { message:
|
|
47
|
+
title: this.language.project.i18n.t('editor:language.symbols.failed', { message: messageOf(error) })
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
}
|
package/src/symbols.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditorBlock } from '@adecore/editor';
|
|
2
|
-
import type { DocumentSymbol, DocumentSymbolResult, SymbolInformation } from '@adecore/lsp';
|
|
2
|
+
import type { DocumentSymbol, DocumentSymbolResult, Range, SymbolInformation } from '@adecore/lsp';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { Refresher } from './refresher.ts';
|
|
5
5
|
import { realTimers, type Timers } from './timers.ts';
|
|
@@ -23,42 +23,43 @@ const KINDS: Record<number, string> = {
|
|
|
23
23
|
23: 'struct'
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
export interface SymbolSpan {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly kind: number;
|
|
29
|
+
readonly range: Range;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function
|
|
31
|
-
const label = KINDS[kind];
|
|
32
|
-
// A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
|
|
33
|
-
return label === undefined || endLine <= startLine ? null : { startLine: startLine + 1, endLine: endLine + 1, name, kind: label };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function walk(symbols: readonly DocumentSymbol[], blocks: EditorBlock[]): void {
|
|
32
|
+
function walk(symbols: readonly DocumentSymbol[], spans: SymbolSpan[]): void {
|
|
37
33
|
for (const symbol of symbols) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
blocks.push(block);
|
|
41
|
-
}
|
|
42
|
-
walk(symbol.children ?? [], blocks);
|
|
34
|
+
spans.push({ name: symbol.name, kind: symbol.kind, range: symbol.range });
|
|
35
|
+
walk(symbol.children ?? [], spans);
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
/*
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
39
|
+
/* Every symbol of an answer, a parent before its children, whether the server sent a tree or a flat list. */
|
|
40
|
+
export function symbolSpansOf(result: DocumentSymbolResult): SymbolSpan[] {
|
|
41
|
+
const spans: SymbolSpan[] = [];
|
|
49
42
|
if (result === null || result.length === 0) {
|
|
50
|
-
return
|
|
43
|
+
return spans;
|
|
51
44
|
}
|
|
52
|
-
if (
|
|
53
|
-
walk(result,
|
|
54
|
-
|
|
55
|
-
for (const symbol of result as SymbolInformation[]) {
|
|
56
|
-
const block = blockOf(symbol.name, symbol.kind, symbol.location.range.start.line, symbol.location.range.end.line);
|
|
57
|
-
if (block !== null) {
|
|
58
|
-
blocks.push(block);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
45
|
+
if ('range' in result[0]!) {
|
|
46
|
+
walk(result as DocumentSymbol[], spans);
|
|
47
|
+
return spans;
|
|
61
48
|
}
|
|
49
|
+
return (result as SymbolInformation[]).map((symbol) => ({ name: symbol.name, kind: symbol.kind, range: symbol.location.range }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function blockOf({ name, kind, range }: SymbolSpan): EditorBlock | null {
|
|
53
|
+
const label = KINDS[kind];
|
|
54
|
+
// A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
|
|
55
|
+
return label === undefined || range.end.line <= range.start.line
|
|
56
|
+
? null
|
|
57
|
+
: { startLine: range.start.line + 1, endLine: range.end.line + 1, name, kind: label };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* The multi-line symbols of a file as blocks, outermost first, in one-based lines. */
|
|
61
|
+
export function blocksOf(result: DocumentSymbolResult): EditorBlock[] {
|
|
62
|
+
const blocks = symbolSpansOf(result).flatMap((span) => blockOf(span) ?? []);
|
|
62
63
|
return blocks.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
63
64
|
}
|
|
64
65
|
|
package/src/testing.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyContentChanges,
|
|
3
|
+
ErrorCodes,
|
|
3
4
|
LspError,
|
|
4
5
|
StaleResultError,
|
|
5
6
|
type LanguageService,
|
|
@@ -69,7 +70,7 @@ export class FakeLanguageService implements LanguageService {
|
|
|
69
70
|
async changeDocument(uri: string, changes: readonly ContentChange[]): Promise<void> {
|
|
70
71
|
const held = this.documents.get(uri);
|
|
71
72
|
if (held === undefined) {
|
|
72
|
-
throw new LspError('Document is not open',
|
|
73
|
+
throw new LspError('Document is not open', ErrorCodes.ServerNotInitialized);
|
|
73
74
|
}
|
|
74
75
|
held.text = applyContentChanges(held.text, changes);
|
|
75
76
|
held.version++;
|
|
@@ -113,7 +114,7 @@ export class FakeLanguageService implements LanguageService {
|
|
|
113
114
|
|
|
114
115
|
private async request<Result>(method: string, uri: string, params: unknown, options: LanguageRequestOptions = {}): Promise<Result> {
|
|
115
116
|
if (!this.supports(method, uri)) {
|
|
116
|
-
throw new LspError(`${method} is unavailable`,
|
|
117
|
+
throw new LspError(`${method} is unavailable`, ErrorCodes.MethodNotFound);
|
|
117
118
|
}
|
|
118
119
|
options.signal?.throwIfAborted();
|
|
119
120
|
const version = this.documents.get(uri)?.version;
|
|
@@ -126,6 +127,7 @@ export class FakeLanguageService implements LanguageService {
|
|
|
126
127
|
}
|
|
127
128
|
return result as Result;
|
|
128
129
|
}
|
|
130
|
+
|
|
129
131
|
completion(uri: string, position: Position, context?: CompletionContext, options: LanguageRequestOptions = {}): Promise<CompletionResult> {
|
|
130
132
|
return this.request<CompletionResult>('textDocument/completion', uri, { position, context }, options);
|
|
131
133
|
}
|
package/src/view-state.ts
CHANGED
package/src/word-diff.ts
CHANGED
|
@@ -4,10 +4,8 @@ import { diffLines } from '@adecore/merge';
|
|
|
4
4
|
const TOKEN = /\w+|\s+|[^\w\s]/g;
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
* removed, since then each removed line has the line it became; any other change has no such pairing
|
|
10
|
-
* and draws no words.
|
|
7
|
+
* The `[start, end)` ranges of each removed line that the added lines replaced. Lines pair up only when as
|
|
8
|
+
* many were added as removed; any other change has no pairing and marks no words.
|
|
11
9
|
*/
|
|
12
10
|
export function replacedWords(removed: readonly string[], added: readonly string[]): Array<Array<[number, number]>> {
|
|
13
11
|
return removed.map((line, index) => (removed.length === added.length ? replacedRanges(line, added[index]!) : []));
|
package/src/workspace-edit.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type WorkspaceEdit
|
|
10
10
|
} from '@adecore/lsp';
|
|
11
11
|
import type { DiskText } from './host-types.ts';
|
|
12
|
+
import { messageOf } from './error-message.ts';
|
|
12
13
|
|
|
13
14
|
/* A file that changed without an editor of this project holding it: its new text waits as an unsaved draft. */
|
|
14
15
|
export interface StagedFile {
|
|
@@ -113,12 +114,10 @@ interface PlannedCreate {
|
|
|
113
114
|
type Planned = PlannedEdits | PlannedCreate | { readonly kind: 'rename'; readonly oldPath: string; readonly newPath: string };
|
|
114
115
|
|
|
115
116
|
/*
|
|
116
|
-
* Applies an LSP workspace edit
|
|
117
|
-
* its edits as one undo step
|
|
118
|
-
* nothing is written to disk on a server's word alone, unless the edit also moves a file:
|
|
119
|
-
*
|
|
120
|
-
* the edit creates is made through the machine with the text the edit writes into it. The edit is checked
|
|
121
|
-
* against every text first, so one that does not fit changes nothing.
|
|
117
|
+
* Applies an LSP workspace edit step by step, after checking it against every text so one that does not fit
|
|
118
|
+
* changes nothing. A file an editor holds takes its edits as one undo step; any other file gets an unsaved
|
|
119
|
+
* draft, since nothing is written to disk on a server's word alone, unless the edit also moves a file: then it
|
|
120
|
+
* is one change, saved and moved through the machine. A file the edit creates is made through the machine.
|
|
122
121
|
*/
|
|
123
122
|
export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult> {
|
|
124
123
|
const steps = stepsOf(edit);
|
|
@@ -206,7 +205,7 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
|
|
|
206
205
|
try {
|
|
207
206
|
created.text = applyTextEdits(created.text, step.edits);
|
|
208
207
|
} catch (error) {
|
|
209
|
-
return
|
|
208
|
+
return messageOf(error);
|
|
210
209
|
}
|
|
211
210
|
continue;
|
|
212
211
|
}
|
|
@@ -228,7 +227,7 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
|
|
|
228
227
|
try {
|
|
229
228
|
after = applyTextEdits(held.text, step.edits);
|
|
230
229
|
} catch (error) {
|
|
231
|
-
return
|
|
230
|
+
return messageOf(error);
|
|
232
231
|
}
|
|
233
232
|
planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
|
|
234
233
|
texts.set(source, { text: after, disk: held.disk });
|