@adecore/editor-react 0.42.0 → 0.44.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 +177 -181
- package/dist/EditorContextMenu.js +118 -105
- package/dist/LanguagePopups.js +7 -0
- package/dist/PickPopup.js +245 -104
- package/dist/WrapCard.d.ts +8 -0
- package/dist/WrapCard.js +172 -0
- package/dist/call-hierarchy-model.d.ts +4 -2
- package/dist/call-hierarchy-model.js +20 -0
- package/dist/call-hierarchy.d.ts +3 -0
- package/dist/call-hierarchy.js +54 -22
- package/dist/clipboard-history.d.ts +31 -0
- package/dist/clipboard-history.js +70 -0
- package/dist/code-actions.d.ts +3 -1
- package/dist/code-actions.js +50 -10
- package/dist/code-vision.js +2 -3
- package/dist/completion.d.ts +28 -0
- package/dist/completion.js +83 -27
- package/dist/context-menu-rows.d.ts +1 -1
- package/dist/context-menu-rows.js +4 -0
- package/dist/definition-link.js +15 -5
- package/dist/document-colors.d.ts +24 -0
- package/dist/document-colors.js +194 -0
- package/dist/document-links.d.ts +22 -0
- package/dist/document-links.js +133 -0
- package/dist/document-query.d.ts +15 -0
- package/dist/document-query.js +54 -0
- package/dist/edit-locations.d.ts +20 -0
- package/dist/edit-locations.js +136 -0
- package/dist/editor-language.d.ts +23 -1
- package/dist/editor-language.js +83 -2
- package/dist/emmet.d.ts +13 -0
- package/dist/emmet.js +62 -0
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +3 -5
- package/dist/highlights.js +2 -3
- package/dist/history.d.ts +7 -0
- package/dist/history.js +79 -1
- package/dist/host-types.d.ts +10 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +10 -0
- package/dist/inlay-hints.js +2 -2
- package/dist/inline-completion.d.ts +39 -0
- package/dist/inline-completion.js +221 -0
- package/dist/key-priority.d.ts +9 -5
- package/dist/key-priority.js +11 -6
- package/dist/linked-editing.d.ts +26 -0
- package/dist/linked-editing.js +215 -0
- package/dist/live-templates.d.ts +16 -0
- package/dist/live-templates.js +289 -0
- package/dist/locales/en.json +61 -2
- package/dist/locales/nl.json +61 -2
- package/dist/markup-context.d.ts +55 -0
- package/dist/markup-context.js +408 -0
- package/dist/markup.d.ts +39 -0
- package/dist/markup.js +465 -0
- package/dist/navigation.js +4 -6
- package/dist/paste-history.d.ts +7 -0
- package/dist/paste-history.js +104 -0
- package/dist/peek.js +3 -4
- package/dist/pick.d.ts +21 -2
- package/dist/pick.js +152 -19
- package/dist/popups.d.ts +10 -0
- package/dist/popups.js +2 -1
- package/dist/postfix.d.ts +21 -0
- package/dist/postfix.js +212 -0
- package/dist/project-language.d.ts +20 -1
- package/dist/project-language.js +132 -18
- package/dist/rename.js +8 -11
- package/dist/rendered-docs.js +1 -2
- package/dist/selection-ranges.js +2 -3
- package/dist/shortcut-keys.d.ts +24 -0
- package/dist/signature.js +3 -6
- package/dist/snippet-insert.d.ts +10 -0
- package/dist/snippet-insert.js +217 -0
- package/dist/snippet-session.d.ts +2 -2
- package/dist/snippet-session.js +3 -3
- package/dist/snippet.d.ts +7 -1
- package/dist/snippet.js +65 -8
- package/dist/surround-constructs.d.ts +10 -0
- package/dist/surround-constructs.js +100 -0
- package/dist/surround.d.ts +18 -0
- package/dist/surround.js +399 -0
- package/dist/symbol-picker.js +4 -6
- package/dist/symbols.js +22 -3
- package/dist/template-builtins.d.ts +3 -0
- package/dist/template-builtins.js +46 -0
- package/dist/template-context.d.ts +12 -0
- package/dist/template-context.js +56 -0
- package/dist/templates.d.ts +59 -0
- package/dist/templates.js +103 -0
- package/dist/testing.d.ts +23 -1
- package/dist/testing.js +114 -0
- package/package.json +7 -6
- package/src/CallHierarchyPanel.tsx +26 -11
- package/src/EditorContextMenu.tsx +7 -0
- package/src/LanguagePopups.tsx +4 -0
- package/src/PickPopup.tsx +47 -5
- package/src/WrapCard.tsx +64 -0
- package/src/call-hierarchy-model.ts +25 -3
- package/src/call-hierarchy.ts +62 -20
- package/src/clipboard-history.ts +102 -0
- package/src/code-actions.ts +48 -11
- package/src/code-vision.ts +3 -3
- package/src/completion.ts +123 -25
- package/src/context-menu-rows.ts +3 -2
- package/src/definition-link.ts +17 -5
- package/src/document-colors.ts +189 -0
- package/src/document-links.ts +129 -0
- package/src/document-query.ts +71 -0
- package/src/edit-locations.ts +162 -0
- package/src/editor-language.ts +86 -3
- package/src/emmet-module.d.ts +44 -0
- package/src/emmet.ts +73 -0
- package/src/folding.ts +3 -3
- package/src/format-on-type.ts +5 -5
- package/src/highlights.ts +3 -3
- package/src/history.ts +81 -1
- package/src/host-types.ts +17 -0
- package/src/index.ts +28 -3
- package/src/inlay-hints.ts +2 -2
- package/src/inline-completion.ts +221 -0
- package/src/key-priority.ts +11 -6
- package/src/linked-editing.ts +199 -0
- package/src/live-templates.ts +315 -0
- package/src/locales/en.json +61 -2
- package/src/locales/nl.json +61 -2
- package/src/markup-context.ts +387 -0
- package/src/markup.ts +377 -0
- package/src/navigation.ts +6 -6
- package/src/paste-history.ts +111 -0
- package/src/peek.ts +4 -4
- package/src/pick.ts +218 -20
- package/src/popups.ts +21 -1
- package/src/postfix.ts +224 -0
- package/src/project-language.ts +139 -19
- package/src/rename.ts +11 -11
- package/src/rendered-docs.ts +2 -2
- package/src/selection-ranges.ts +3 -3
- package/src/signature.ts +6 -6
- package/src/snippet-insert.ts +258 -0
- package/src/snippet-session.ts +9 -4
- package/src/snippet.ts +82 -7
- package/src/surround-constructs.ts +85 -0
- package/src/surround.ts +430 -0
- package/src/symbol-picker.ts +6 -6
- package/src/symbols.ts +30 -4
- package/src/template-builtins.ts +94 -0
- package/src/template-context.ts +65 -0
- package/src/templates.ts +197 -0
- package/src/testing.ts +126 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
2
|
+
import type { Timers } from './timers.ts';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Asks the servers for something of the whole document: once it is open, a moment after the last edit, and
|
|
6
|
+
* whenever what the servers offer changes. An edit clears the answer at once, since its ranges point into a
|
|
7
|
+
* text that is gone; an answer for an older text is dropped.
|
|
8
|
+
*/
|
|
9
|
+
export class DocumentQuery<Value> {
|
|
10
|
+
private readonly language: EditorLanguage;
|
|
11
|
+
private readonly timers: Timers;
|
|
12
|
+
private readonly delay: number;
|
|
13
|
+
private readonly ask: (signal: AbortSignal) => Promise<Value | null>;
|
|
14
|
+
private readonly apply: (value: Value | null) => void;
|
|
15
|
+
private timer: unknown = null;
|
|
16
|
+
private controller: AbortController | null = null;
|
|
17
|
+
private generation = 0;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
language: EditorLanguage,
|
|
21
|
+
timers: Timers,
|
|
22
|
+
delay: number,
|
|
23
|
+
ask: (signal: AbortSignal) => Promise<Value | null>,
|
|
24
|
+
apply: (value: Value | null) => void
|
|
25
|
+
) {
|
|
26
|
+
this.language = language;
|
|
27
|
+
this.timers = timers;
|
|
28
|
+
this.delay = delay;
|
|
29
|
+
this.ask = ask;
|
|
30
|
+
this.apply = apply;
|
|
31
|
+
const offs = [
|
|
32
|
+
language.onProvidersChanged(() => this.run()),
|
|
33
|
+
language.editor.onTextChange(() => {
|
|
34
|
+
this.cancel();
|
|
35
|
+
this.apply(null);
|
|
36
|
+
this.timer = this.timers.set(() => this.run(), this.delay);
|
|
37
|
+
})
|
|
38
|
+
];
|
|
39
|
+
language.onDispose(() => {
|
|
40
|
+
this.cancel();
|
|
41
|
+
for (const off of offs) {
|
|
42
|
+
off();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Asks again now, as a refresh of the host does. */
|
|
48
|
+
run(): void {
|
|
49
|
+
this.cancel();
|
|
50
|
+
const generation = this.generation;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
this.controller = controller;
|
|
53
|
+
this.ask(controller.signal).then(
|
|
54
|
+
(value) => {
|
|
55
|
+
if (generation === this.generation && !this.language.isDisposed) {
|
|
56
|
+
this.apply(value);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
// A server that fails or refuses answers nothing; the next edit asks again.
|
|
60
|
+
() => undefined
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private cancel(): void {
|
|
65
|
+
this.generation++;
|
|
66
|
+
this.timers.clear(this.timer);
|
|
67
|
+
this.timer = null;
|
|
68
|
+
this.controller?.abort();
|
|
69
|
+
this.controller = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
+
|
|
3
|
+
/* Where an edit left the carets of one file, the primary last. */
|
|
4
|
+
export interface EditPlace {
|
|
5
|
+
readonly uri: string;
|
|
6
|
+
readonly selections: readonly EditorRange[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LIMIT = 150;
|
|
10
|
+
|
|
11
|
+
/* Two places are one when they are in one file with as many carets, and every caret is within three lines of its twin. */
|
|
12
|
+
function isSame(left: EditPlace, right: EditPlace | null): boolean {
|
|
13
|
+
return (
|
|
14
|
+
right !== null &&
|
|
15
|
+
left.uri === right.uri &&
|
|
16
|
+
left.selections.length === right.selections.length &&
|
|
17
|
+
left.selections.every((selection, index) => Math.abs(selection.end.line - right.selections[index]!.end.line) < 4)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function before(left: EditorPosition, right: EditorPosition): boolean {
|
|
22
|
+
return left.line < right.line || (left.line === right.line && left.character < right.character);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Where the text of a change ends, in the text after the change. */
|
|
26
|
+
function endOf(change: EditorContentChange): EditorPosition {
|
|
27
|
+
const lines = change.text.split('\n');
|
|
28
|
+
const { start } = change.range;
|
|
29
|
+
|
|
30
|
+
return lines.length === 1
|
|
31
|
+
? { line: start.line, character: start.character + change.text.length }
|
|
32
|
+
: { line: start.line + lines.length - 1, character: lines[lines.length - 1]!.length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* A position after a change: untouched in front of it, at the start when the change replaced the text around it, moved along behind it. */
|
|
36
|
+
function follow(position: EditorPosition, change: EditorContentChange): EditorPosition {
|
|
37
|
+
const { start, end } = change.range;
|
|
38
|
+
|
|
39
|
+
if (!before(start, position)) {
|
|
40
|
+
return position;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (before(position, end)) {
|
|
44
|
+
return start;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const landed = endOf(change);
|
|
48
|
+
|
|
49
|
+
return position.line === end.line
|
|
50
|
+
? { line: landed.line, character: landed.character + position.character - end.character }
|
|
51
|
+
: { line: position.line + landed.line - end.line, character: position.character };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* Where the project was edited, for Last Edit Location and Next Edit Location. An edit within three lines of the
|
|
56
|
+
* last one is the same place, so typing a paragraph is one stop. A caret that moves without an edit puts the walk
|
|
57
|
+
* back at the end, and so does an edit.
|
|
58
|
+
*/
|
|
59
|
+
export class EditLocations {
|
|
60
|
+
private readonly limit: number;
|
|
61
|
+
private entries: EditPlace[] = [];
|
|
62
|
+
private index = 0;
|
|
63
|
+
private arriving: { uri: string; line: number } | null = null;
|
|
64
|
+
|
|
65
|
+
constructor(limit = LIMIT) {
|
|
66
|
+
this.limit = limit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get places(): readonly EditPlace[] {
|
|
70
|
+
return this.entries;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* An edit left the carets here. */
|
|
74
|
+
record(place: EditPlace): void {
|
|
75
|
+
const last = this.entries.at(-1);
|
|
76
|
+
|
|
77
|
+
if (last !== undefined && isSame(last, place)) {
|
|
78
|
+
this.entries[this.entries.length - 1] = place;
|
|
79
|
+
} else {
|
|
80
|
+
this.entries.push(place);
|
|
81
|
+
|
|
82
|
+
if (this.entries.length > this.limit) {
|
|
83
|
+
this.entries.shift();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.index = this.entries.length;
|
|
88
|
+
this.arriving = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* The caret moved without an edit. The move that lands on the place a walk went to does not count. */
|
|
92
|
+
moved(uri: string, position: EditorPosition): void {
|
|
93
|
+
if (this.arriving?.uri === uri && this.arriving.line === position.line) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.arriving = null;
|
|
98
|
+
this.index = this.entries.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The place of an earlier edit that is not the one the carets are on; null when there is none. */
|
|
102
|
+
previous(current: EditPlace | null): EditPlace | null {
|
|
103
|
+
for (let i = this.index - 1; i >= 0; i--) {
|
|
104
|
+
if (!isSame(this.entries[i]!, current)) {
|
|
105
|
+
this.index = i;
|
|
106
|
+
|
|
107
|
+
return this.arrive(this.entries[i]!);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* The place of a later edit; it moves one past the place it returns, so Last Edit Location and Next Edit Location are not exact opposites. */
|
|
115
|
+
next(current: EditPlace | null): EditPlace | null {
|
|
116
|
+
for (let i = this.index; i < this.entries.length; i++) {
|
|
117
|
+
if (!isSame(this.entries[i]!, current)) {
|
|
118
|
+
this.index = i + 1;
|
|
119
|
+
|
|
120
|
+
return this.arrive(this.entries[i]!);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* The places of a file follow its edits, in the order the editor made them. */
|
|
128
|
+
map(uri: string, changes: readonly EditorContentChange[]): void {
|
|
129
|
+
if (!this.entries.some((entry) => entry.uri === uri)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.entries = this.entries.map((entry) => {
|
|
134
|
+
if (entry.uri !== uri) {
|
|
135
|
+
return entry;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const selections = changes.reduce(
|
|
139
|
+
(current, change) => current.map((selection) => ({ start: follow(selection.start, change), end: follow(selection.end, change) })),
|
|
140
|
+
entry.selections
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return { uri, selections };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Drops the places of files that are gone. */
|
|
148
|
+
forget(matches: (uri: string) => boolean): void {
|
|
149
|
+
this.entries = this.entries.filter((entry) => !matches(entry.uri));
|
|
150
|
+
this.index = this.entries.length;
|
|
151
|
+
|
|
152
|
+
if (this.arriving !== null && matches(this.arriving.uri)) {
|
|
153
|
+
this.arriving = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private arrive(place: EditPlace): EditPlace {
|
|
158
|
+
this.arriving = { uri: place.uri, line: place.selections.at(-1)!.end.line };
|
|
159
|
+
|
|
160
|
+
return place;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/editor-language.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { KEYMAP, type Keymap } from '@adecore/editor/keymap';
|
|
2
2
|
import type { Editor, EditorKeyHandler, EditorPosition } from '@adecore/editor';
|
|
3
|
-
import type { Location } from '@adecore/lsp';
|
|
3
|
+
import type { LanguageService, Location, TextDocumentSaveReason } from '@adecore/lsp';
|
|
4
|
+
import { DocumentColorsFeature } from './document-colors.ts';
|
|
5
|
+
import { DocumentLinksFeature } from './document-links.ts';
|
|
6
|
+
import { InlineCompletionFeature } from './inline-completion.ts';
|
|
7
|
+
import { LinkedEditingFeature } from './linked-editing.ts';
|
|
4
8
|
import { CallHierarchyFeature } from './call-hierarchy.ts';
|
|
5
9
|
import { CodeActionsFeature } from './code-actions.ts';
|
|
6
10
|
import { CodeVisionFeature } from './code-vision.ts';
|
|
@@ -14,7 +18,9 @@ import { HighlightsFeature } from './highlights.ts';
|
|
|
14
18
|
import { HistoryFeature } from './history.ts';
|
|
15
19
|
import { HoverFeature } from './hover.ts';
|
|
16
20
|
import { KeyDispatcher } from './key-priority.ts';
|
|
21
|
+
import { MarkupFeature } from './markup.ts';
|
|
17
22
|
import { NavigationFeature, locationRow } from './navigation.ts';
|
|
23
|
+
import { PasteHistoryFeature } from './paste-history.ts';
|
|
18
24
|
import { PeekFeature } from './peek.ts';
|
|
19
25
|
import { PickFeature } from './pick.ts';
|
|
20
26
|
import { SymbolPickerFeature } from './symbol-picker.ts';
|
|
@@ -28,14 +34,21 @@ import { SelectionRangesFeature } from './selection-ranges.ts';
|
|
|
28
34
|
import { SemanticTokensFeature, type SemanticTokensOptions } from './semantic-tokens.ts';
|
|
29
35
|
import { SignatureFeature } from './signature.ts';
|
|
30
36
|
import { SnippetFeature } from './snippet-session.ts';
|
|
37
|
+
import { SurroundFeature } from './surround.ts';
|
|
31
38
|
import { SymbolsFeature } from './symbols.ts';
|
|
39
|
+
import { TemplatesFeature } from './live-templates.ts';
|
|
32
40
|
import type { Place } from './navigation-history.ts';
|
|
33
41
|
import type { LanguageDocumentHandle, ProjectLanguage } from './project-language.ts';
|
|
34
42
|
|
|
43
|
+
/* How long the edits servers want before a save are waited for. */
|
|
44
|
+
const SAVE_PARTICIPANT_MS = 1_000;
|
|
45
|
+
|
|
35
46
|
/* What an app tunes about the features of one editor. */
|
|
36
47
|
export interface EditorLanguageOptions {
|
|
37
48
|
/* Where a document counts as large for semantic tokens. */
|
|
38
49
|
semanticTokens?: SemanticTokensOptions;
|
|
50
|
+
/* False for a document no server serves: it counts as open in the project and the features stay inert. True by default. */
|
|
51
|
+
serve?: boolean;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
/*
|
|
@@ -51,6 +64,9 @@ export class EditorLanguage {
|
|
|
51
64
|
readonly document: LanguageDocumentHandle;
|
|
52
65
|
readonly languageId: string;
|
|
53
66
|
|
|
67
|
+
/* Whether a server serves this document; read it where a host meant "a language server is behind this editor". */
|
|
68
|
+
readonly served: boolean;
|
|
69
|
+
|
|
54
70
|
/* What the features of this editor have open over it: the hover card, the suggestions, the signature. */
|
|
55
71
|
readonly popups = createPopupStore();
|
|
56
72
|
|
|
@@ -60,6 +76,9 @@ export class EditorLanguage {
|
|
|
60
76
|
readonly completion: CompletionFeature;
|
|
61
77
|
readonly signature: SignatureFeature;
|
|
62
78
|
readonly snippets: SnippetFeature;
|
|
79
|
+
readonly markup: MarkupFeature;
|
|
80
|
+
readonly templates: TemplatesFeature;
|
|
81
|
+
readonly surround: SurroundFeature;
|
|
63
82
|
readonly highlights: HighlightsFeature;
|
|
64
83
|
readonly pick: PickFeature;
|
|
65
84
|
readonly codeActions: CodeActionsFeature;
|
|
@@ -75,8 +94,14 @@ export class EditorLanguage {
|
|
|
75
94
|
readonly codeVision: CodeVisionFeature;
|
|
76
95
|
readonly formatOnType: FormatOnTypeFeature;
|
|
77
96
|
readonly renderedDocs: RenderedDocsFeature;
|
|
97
|
+
readonly pasteHistory: PasteHistoryFeature;
|
|
98
|
+
readonly links: DocumentLinksFeature;
|
|
99
|
+
readonly colors: DocumentColorsFeature;
|
|
100
|
+
readonly linkedEditing: LinkedEditingFeature;
|
|
101
|
+
readonly inlineCompletion: InlineCompletionFeature;
|
|
78
102
|
private readonly disposers: Array<() => void> = [];
|
|
79
103
|
private readonly keys = new KeyDispatcher();
|
|
104
|
+
private readonly timers: Timers;
|
|
80
105
|
private disposed = false;
|
|
81
106
|
|
|
82
107
|
constructor(project: ProjectLanguage, editor: Editor, uri: string, languageId: string, timers: Timers = realTimers, options: EditorLanguageOptions = {}) {
|
|
@@ -86,13 +111,18 @@ export class EditorLanguage {
|
|
|
86
111
|
this.shortcuts = shortcutsOf(this.keymap, this.apple);
|
|
87
112
|
this.project = project;
|
|
88
113
|
this.languageId = languageId;
|
|
89
|
-
this.
|
|
114
|
+
this.served = options.serve ?? true;
|
|
115
|
+
this.document = project.acquire(uri, languageId, editor, { serve: this.served });
|
|
116
|
+
this.timers = timers;
|
|
90
117
|
this.onDispose(editor.onKeyDown((event) => this.keys.dispatch(event)));
|
|
91
118
|
this.pick = new PickFeature(this, timers);
|
|
92
119
|
this.diagnostics = new DiagnosticsFeature(this);
|
|
93
120
|
this.hover = new HoverFeature(this, timers);
|
|
94
121
|
this.completion = new CompletionFeature(this, timers);
|
|
95
122
|
this.snippets = new SnippetFeature(this, timers);
|
|
123
|
+
this.markup = new MarkupFeature(this);
|
|
124
|
+
this.templates = new TemplatesFeature(this);
|
|
125
|
+
this.surround = new SurroundFeature(this);
|
|
96
126
|
this.signature = new SignatureFeature(this, timers);
|
|
97
127
|
this.highlights = new HighlightsFeature(this, timers);
|
|
98
128
|
this.symbols = new SymbolsFeature(this, timers);
|
|
@@ -101,6 +131,11 @@ export class EditorLanguage {
|
|
|
101
131
|
this.inlayHints = new InlayHintsFeature(this, timers);
|
|
102
132
|
this.codeActions = new CodeActionsFeature(this, timers);
|
|
103
133
|
this.rename = new RenameFeature(this);
|
|
134
|
+
// Before navigation, whose Mod+click goes to a definition: a link under the pointer goes first.
|
|
135
|
+
this.links = new DocumentLinksFeature(this, timers);
|
|
136
|
+
this.colors = new DocumentColorsFeature(this, timers);
|
|
137
|
+
this.linkedEditing = new LinkedEditingFeature(this, timers);
|
|
138
|
+
this.inlineCompletion = new InlineCompletionFeature(this, timers);
|
|
104
139
|
this.navigation = new NavigationFeature(this);
|
|
105
140
|
this.peek = new PeekFeature(this, timers);
|
|
106
141
|
this.callHierarchy = new CallHierarchyFeature(this, timers);
|
|
@@ -111,7 +146,17 @@ export class EditorLanguage {
|
|
|
111
146
|
this.codeVision = new CodeVisionFeature(this, (listener) => this.symbols.onResult(listener), timers);
|
|
112
147
|
this.formatOnType = new FormatOnTypeFeature(this);
|
|
113
148
|
this.renderedDocs = new RenderedDocsFeature(this, timers);
|
|
149
|
+
this.pasteHistory = new PasteHistoryFeature(this);
|
|
114
150
|
new FoldingFeature(this, (listener) => this.symbols.onResult(listener), timers);
|
|
151
|
+
|
|
152
|
+
const copies = editor.onCopy?.((copy) => project.clipboard.add(copy.text, { wholeLines: copy.wholeLines }));
|
|
153
|
+
|
|
154
|
+
if (copies !== undefined) {
|
|
155
|
+
this.onDispose(copies);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
editor.setWordSources?.(() => this.otherTexts());
|
|
159
|
+
this.onDispose(() => editor.setWordSources?.(null));
|
|
115
160
|
this.placeOpenedCaret(editor.getCaret());
|
|
116
161
|
this.onDispose(editor.onCaret((position) => this.placeOpenedCaret(position)));
|
|
117
162
|
}
|
|
@@ -120,6 +165,11 @@ export class EditorLanguage {
|
|
|
120
165
|
return this.document.uri;
|
|
121
166
|
}
|
|
122
167
|
|
|
168
|
+
/* The service this document is asked through: the project's, or one that answers nothing when no server serves it. */
|
|
169
|
+
get service(): LanguageService {
|
|
170
|
+
return this.document.service;
|
|
171
|
+
}
|
|
172
|
+
|
|
123
173
|
get isDisposed(): boolean {
|
|
124
174
|
return this.disposed;
|
|
125
175
|
}
|
|
@@ -173,7 +223,7 @@ export class EditorLanguage {
|
|
|
173
223
|
() => undefined
|
|
174
224
|
);
|
|
175
225
|
|
|
176
|
-
const providers = this.
|
|
226
|
+
const providers = this.service.onProvidersChanged((uri) => {
|
|
177
227
|
if (uri === this.uri) {
|
|
178
228
|
listener();
|
|
179
229
|
}
|
|
@@ -193,6 +243,31 @@ export class EditorLanguage {
|
|
|
193
243
|
return this.keys.add(handler, priority);
|
|
194
244
|
}
|
|
195
245
|
|
|
246
|
+
/*
|
|
247
|
+
* Lets the servers in before the host writes the file: tells them a save is coming and applies the edits they
|
|
248
|
+
* want made first, such as organized imports. A server that has not answered within a second is passed over,
|
|
249
|
+
* as the specification allows. Call it, then save, then let the service send `didSave`.
|
|
250
|
+
*/
|
|
251
|
+
async prepareSave(reason: TextDocumentSaveReason = 1): Promise<void> {
|
|
252
|
+
const { service } = this.project;
|
|
253
|
+
await service.willSave?.(this.uri, reason).catch(() => undefined);
|
|
254
|
+
if (service.willSaveWaitUntil === undefined) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
const timer = this.timers.set(() => controller.abort(), SAVE_PARTICIPANT_MS);
|
|
259
|
+
try {
|
|
260
|
+
const edits = await service.willSaveWaitUntil(this.uri, reason, { signal: controller.signal });
|
|
261
|
+
if (!controller.signal.aborted && edits !== null && edits.length > 0) {
|
|
262
|
+
this.editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })));
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// A server that fails or is too slow does not keep the file from being saved.
|
|
266
|
+
} finally {
|
|
267
|
+
this.timers.clear(timer);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
196
271
|
/* Runs `dispose` when the editor lets go of its document. */
|
|
197
272
|
onDispose(dispose: () => void): void {
|
|
198
273
|
this.disposers.push(dispose);
|
|
@@ -212,6 +287,14 @@ export class EditorLanguage {
|
|
|
212
287
|
this.document.release();
|
|
213
288
|
}
|
|
214
289
|
|
|
290
|
+
/* The texts of the other open editors of the project, which Hippie completion takes words from. */
|
|
291
|
+
private otherTexts(): string[] {
|
|
292
|
+
return this.project
|
|
293
|
+
.openEditors()
|
|
294
|
+
.filter((open) => open.uri !== this.uri)
|
|
295
|
+
.map((open) => open.editor.getText());
|
|
296
|
+
}
|
|
297
|
+
|
|
215
298
|
/* A file opened from another one lands on the line; this puts the caret on the column the jump meant. */
|
|
216
299
|
private placeOpenedCaret(position: EditorPosition): void {
|
|
217
300
|
const column = this.project.takeCaret(this.uri, position.line);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The package ships `dist/index.d.ts`, but its `exports` point TypeScript at none, so the part used here is declared again.
|
|
2
|
+
declare module 'emmet' {
|
|
3
|
+
export interface MarkupAbbreviationNode {
|
|
4
|
+
name?: string;
|
|
5
|
+
children: MarkupAbbreviationNode[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MarkupAbbreviation {
|
|
9
|
+
type: 'Abbreviation';
|
|
10
|
+
children: MarkupAbbreviationNode[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExtractedAbbreviation {
|
|
14
|
+
abbreviation: string;
|
|
15
|
+
location: number;
|
|
16
|
+
start: number;
|
|
17
|
+
end: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ExtractOptions {
|
|
21
|
+
lookAhead: boolean;
|
|
22
|
+
type: 'markup' | 'stylesheet';
|
|
23
|
+
prefix: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UserConfig {
|
|
27
|
+
syntax?: string;
|
|
28
|
+
text?: string | string[];
|
|
29
|
+
options?: {
|
|
30
|
+
'output.field'?: (index: number, placeholder: string, offset: number, line: number, column: number) => string;
|
|
31
|
+
'output.text'?: (text: string, offset: number, line: number, column: number) => string;
|
|
32
|
+
'output.indent'?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Config extends UserConfig {
|
|
37
|
+
type: 'markup' | 'stylesheet';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extract(line: string, pos?: number, options?: Partial<ExtractOptions>): ExtractedAbbreviation | undefined;
|
|
41
|
+
export function markupAbbreviation(abbreviation: string, options?: { text?: string | string[] }): MarkupAbbreviation;
|
|
42
|
+
export function markup(abbreviation: string | MarkupAbbreviation, config: Config): string;
|
|
43
|
+
export function resolveConfig(config?: UserConfig): Config;
|
|
44
|
+
}
|
package/src/emmet.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A reference and no import, so an app that type-checks this source reads the declaration of the package too.
|
|
2
|
+
// oxlint-disable-next-line typescript/triple-slash-reference
|
|
3
|
+
/// <reference path="./emmet-module.d.ts" />
|
|
4
|
+
import { extract, markup, markupAbbreviation, resolveConfig, type MarkupAbbreviationNode } from 'emmet';
|
|
5
|
+
import type { MarkupSyntax } from './markup-context.ts';
|
|
6
|
+
|
|
7
|
+
export interface AbbreviationOptions {
|
|
8
|
+
readonly syntax: MarkupSyntax;
|
|
9
|
+
/* One level of indentation inside the expansion. */
|
|
10
|
+
readonly indent: string;
|
|
11
|
+
/* Lines the abbreviation wraps; a repeated element without a count takes one line each. */
|
|
12
|
+
readonly wrap?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FoundAbbreviation {
|
|
16
|
+
readonly abbreviation: string;
|
|
17
|
+
/* UTF-16 columns of the line, `end` past the caret when brackets the editor closed follow it. */
|
|
18
|
+
readonly start: number;
|
|
19
|
+
readonly end: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* A text of an expansion is snippet syntax, where these mean a tab stop or end one. */
|
|
23
|
+
function escapeText(text: string): string {
|
|
24
|
+
return text.replace(/[$}\\]/g, '\\$&');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function field(index: number, placeholder: string): string {
|
|
28
|
+
return placeholder === '' ? `\${${index}}` : `\${${index}:${escapeText(placeholder)}}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* `lipsum` is the other name of `lorem` that IntelliJ and the Emmet documentation accept. */
|
|
32
|
+
function renameLipsum(node: MarkupAbbreviationNode): void {
|
|
33
|
+
if (node.name !== undefined && /^lipsum(?=\d|$)/i.test(node.name)) {
|
|
34
|
+
node.name = 'lorem' + node.name.slice(6);
|
|
35
|
+
}
|
|
36
|
+
for (const child of node.children) {
|
|
37
|
+
renameLipsum(child);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* The abbreviation that ends at `character` of a line, or null where nothing before the caret reads as one. */
|
|
42
|
+
export function abbreviationBefore(line: string, character: number): FoundAbbreviation | null {
|
|
43
|
+
const found = extract(line, character, { type: 'markup', lookAhead: true });
|
|
44
|
+
if (found === undefined || !/^[A-Za-z!.#([{]/.test(found.abbreviation)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { abbreviation: found.abbreviation, start: found.start, end: found.end };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* The expansion of an abbreviation as snippet text, its places to fill as tab stops; null when it does not
|
|
52
|
+
* read as one. Lorem ipsum words after the first sentence differ per expansion.
|
|
53
|
+
*/
|
|
54
|
+
export function expandAbbreviation(abbreviation: string, options: AbbreviationOptions): string | null {
|
|
55
|
+
const text = options.wrap === undefined ? undefined : [...options.wrap];
|
|
56
|
+
try {
|
|
57
|
+
const tree = markupAbbreviation(abbreviation, { text });
|
|
58
|
+
for (const child of tree.children) {
|
|
59
|
+
renameLipsum(child);
|
|
60
|
+
}
|
|
61
|
+
const expanded = markup(
|
|
62
|
+
tree,
|
|
63
|
+
resolveConfig({
|
|
64
|
+
syntax: options.syntax === 'xml' ? 'xml' : 'html',
|
|
65
|
+
text,
|
|
66
|
+
options: { 'output.field': field, 'output.text': escapeText, 'output.indent': options.indent }
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
return expanded === '' ? null : expanded;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/folding.ts
CHANGED
|
@@ -58,15 +58,15 @@ export class FoldingFeature {
|
|
|
58
58
|
|
|
59
59
|
constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
|
|
60
60
|
this.language = language;
|
|
61
|
-
const { editor,
|
|
61
|
+
const { editor, uri } = language;
|
|
62
62
|
|
|
63
63
|
this.refresher = new Refresher(
|
|
64
64
|
async (signal) => {
|
|
65
|
-
if (!
|
|
65
|
+
if (!language.service.supports(METHOD, uri)) {
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const ranges = await
|
|
69
|
+
const ranges = await language.service.foldingRanges(uri, { signal });
|
|
70
70
|
|
|
71
71
|
if (!signal.aborted) {
|
|
72
72
|
this.ranges = foldRangesOf(ranges);
|
package/src/format-on-type.ts
CHANGED
|
@@ -52,9 +52,9 @@ export class FormatOnTypeFeature {
|
|
|
52
52
|
|
|
53
53
|
/* Whether the server of this document formats on type and the service can ask it. */
|
|
54
54
|
get supported(): boolean {
|
|
55
|
-
const {
|
|
55
|
+
const { uri } = this.language;
|
|
56
56
|
|
|
57
|
-
return
|
|
57
|
+
return this.language.service.onTypeFormatting !== undefined && this.language.service.supports(METHOD, uri);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
configure(enabled: boolean): void {
|
|
@@ -67,7 +67,7 @@ export class FormatOnTypeFeature {
|
|
|
67
67
|
|
|
68
68
|
/* The characters that ask the server, the first one and the more. */
|
|
69
69
|
triggerCharacters(): readonly string[] {
|
|
70
|
-
const options = this.language.
|
|
70
|
+
const options = this.language.service.providerOptions(METHOD, this.language.uri);
|
|
71
71
|
|
|
72
72
|
return options === undefined
|
|
73
73
|
? []
|
|
@@ -99,12 +99,12 @@ export class FormatOnTypeFeature {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
private async ask(revision: number, typed: string, caret: { line: number; character: number }, signal: AbortSignal): Promise<void> {
|
|
102
|
-
const { editor,
|
|
102
|
+
const { editor, uri, document } = this.language;
|
|
103
103
|
|
|
104
104
|
try {
|
|
105
105
|
// The change of the typed character reaches the server before this question does.
|
|
106
106
|
await document.ready;
|
|
107
|
-
const edits = await
|
|
107
|
+
const edits = await this.language.service.onTypeFormatting!(uri, caret, typed, { ...editor.getIndentation() }, { signal });
|
|
108
108
|
const here = editor.getCaret();
|
|
109
109
|
const stale = revision !== this.revision || this.language.isDisposed || comparePositions(here, caret) !== 0;
|
|
110
110
|
|
package/src/highlights.ts
CHANGED
|
@@ -46,17 +46,17 @@ export class HighlightsFeature {
|
|
|
46
46
|
|
|
47
47
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
48
48
|
this.language = language;
|
|
49
|
-
const { editor,
|
|
49
|
+
const { editor, uri } = language;
|
|
50
50
|
|
|
51
51
|
this.refresher = new Refresher(
|
|
52
52
|
async (signal) => {
|
|
53
|
-
if (!
|
|
53
|
+
if (!language.service.supports(METHOD, uri) || !this.onName()) {
|
|
54
54
|
this.clear();
|
|
55
55
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const result = await
|
|
59
|
+
const result = await language.service.documentHighlights(uri, editor.getCaret(), { signal });
|
|
60
60
|
|
|
61
61
|
if (!signal.aborted) {
|
|
62
62
|
this.set(result === null ? [] : editorHighlightsOf(result));
|