@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
package/src/history.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EditorPosition, EditorTextChange } from '@adecore/editor';
|
|
2
|
+
import type { EditPlace } from './edit-locations.ts';
|
|
1
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
2
4
|
import { KEY_PRIORITY } from './key-priority.ts';
|
|
3
5
|
import type { Place } from './navigation-history.ts';
|
|
@@ -6,6 +8,7 @@ import { isShortcut } from './shortcut-keys.ts';
|
|
|
6
8
|
/* Back, Forward and the list of recent locations over the history the whole project shares. */
|
|
7
9
|
export class HistoryFeature {
|
|
8
10
|
private readonly language: EditorLanguage;
|
|
11
|
+
private editing = false;
|
|
9
12
|
|
|
10
13
|
constructor(language: EditorLanguage) {
|
|
11
14
|
this.language = language;
|
|
@@ -29,10 +32,42 @@ export class HistoryFeature {
|
|
|
29
32
|
return true;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
if (isShortcut(this.language.shortcuts.lastEditLocation, event)) {
|
|
36
|
+
this.lastEdit();
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isShortcut(this.language.shortcuts.nextEditLocation, event)) {
|
|
42
|
+
this.nextEdit();
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
return false;
|
|
33
48
|
}, KEY_PRIORITY.history);
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
// A jump of the editor's own, such as Method Down, is a place Back returns to like any other jump.
|
|
51
|
+
const navigations = language.editor.onNavigate?.((from) => language.project.history.record({ uri: language.uri, position: from }));
|
|
52
|
+
const changes = language.editor.onTextChange((change) => this.edited(change));
|
|
53
|
+
const carets = language.editor.onCaret((position) => this.moved(position));
|
|
54
|
+
|
|
55
|
+
language.onDispose(() => {
|
|
56
|
+
off();
|
|
57
|
+
navigations?.();
|
|
58
|
+
changes();
|
|
59
|
+
carets();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Goes to where the project was edited before the carets' place; false when there is no such place. */
|
|
64
|
+
lastEdit(): boolean {
|
|
65
|
+
return this.visitEdit(this.language.project.edits.previous(this.currentEdit()));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Goes to where the project was edited after the place Last Edit Location came back to. */
|
|
69
|
+
nextEdit(): boolean {
|
|
70
|
+
return this.visitEdit(this.language.project.edits.next(this.currentEdit()));
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
/* Returns to where the caret was before the last jump; false when there is no such place. */
|
|
@@ -55,6 +90,51 @@ export class HistoryFeature {
|
|
|
55
90
|
);
|
|
56
91
|
}
|
|
57
92
|
|
|
93
|
+
private currentEdit(): EditPlace {
|
|
94
|
+
return { uri: this.language.uri, selections: this.language.editor.getSelections() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Every edit moves the places of the file; one a person made counts as an edit location once the caret has settled. */
|
|
98
|
+
private edited(change: EditorTextChange): void {
|
|
99
|
+
const { editor, project, uri } = this.language;
|
|
100
|
+
|
|
101
|
+
project.edits.map(uri, change.changes);
|
|
102
|
+
this.editing = true;
|
|
103
|
+
|
|
104
|
+
queueMicrotask(() => {
|
|
105
|
+
this.editing = false;
|
|
106
|
+
|
|
107
|
+
if (change.source !== 'external' && editor.hasFocus?.() !== false && !this.language.isDisposed) {
|
|
108
|
+
project.edits.record(this.currentEdit());
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* A caret that moves on its own ends the walk through the edits, unless an edit moved it. */
|
|
114
|
+
private moved(position: EditorPosition): void {
|
|
115
|
+
if (!this.editing) {
|
|
116
|
+
this.language.project.edits.moved(this.language.uri, position);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private visitEdit(place: EditPlace | null): boolean {
|
|
121
|
+
if (place === null) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { editor, project, uri } = this.language;
|
|
126
|
+
|
|
127
|
+
if (place.uri === uri) {
|
|
128
|
+
editor.setSelections(place.selections, 'center');
|
|
129
|
+
editor.focus();
|
|
130
|
+
} else {
|
|
131
|
+
// Another file opens on one caret, the primary.
|
|
132
|
+
project.openPlace({ uri: place.uri, position: place.selections.at(-1)!.end });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
58
138
|
private visit(place: Place | null): boolean {
|
|
59
139
|
if (place === null) {
|
|
60
140
|
return false;
|
package/src/host-types.ts
CHANGED
|
@@ -2,9 +2,12 @@ import type { WorkerRequestPort } from '@adecore/editor/worker-client';
|
|
|
2
2
|
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
3
|
import type { i18n } from 'i18next';
|
|
4
4
|
import type { Keymap } from '@adecore/editor/keymap';
|
|
5
|
+
import type { ClipboardHistory } from './clipboard-history.ts';
|
|
5
6
|
import type { ProjectFiles } from './workspace-edit.ts';
|
|
6
7
|
import type { Place } from './navigation-history.ts';
|
|
7
8
|
import type { ProblemSource } from './project-problems.ts';
|
|
9
|
+
import type { LiveTemplateDraft, TemplateRegistry } from './templates.ts';
|
|
10
|
+
import type { ServerMessage } from '@adecore/lsp';
|
|
8
11
|
|
|
9
12
|
export interface DiskText {
|
|
10
13
|
text: string;
|
|
@@ -64,6 +67,16 @@ export interface LanguageHost {
|
|
|
64
67
|
readonly files?: ProjectFiles;
|
|
65
68
|
readonly apple?: boolean;
|
|
66
69
|
readonly keymap?: Keymap;
|
|
70
|
+
/* What Paste from History lists. A new `ClipboardHistory` that lives as long as the project without one; pass one to choose its size or to keep its entries. */
|
|
71
|
+
readonly clipboard?: ClipboardHistory;
|
|
72
|
+
/* What templates the editors of the project know. A registry of the built-in ones that lives as long as the project without one. */
|
|
73
|
+
readonly templates?: TemplateRegistry;
|
|
74
|
+
/* Save as Live Template: the host opens its own form with this draft. Without it the command does nothing. */
|
|
75
|
+
saveAsLiveTemplate?(draft: LiveTemplateDraft): void;
|
|
76
|
+
/* The last row of the template lists, "Configure live templates…"; the row is left out without it. */
|
|
77
|
+
openTemplateSettings?(): void;
|
|
78
|
+
/* How many places Last Edit Location and Next Edit Location remember; 150 without one. */
|
|
79
|
+
readonly editHistoryLimit?: number;
|
|
67
80
|
/* The height in pixels of the list-and-preview area of the peek and the call hierarchy, so a person's drag outlives the panel; 180 without one. */
|
|
68
81
|
readonly panelBodyHeight?: number;
|
|
69
82
|
/* A person finished resizing a peek or call hierarchy panel; the height is already within what the editor allows. */
|
|
@@ -71,9 +84,13 @@ export interface LanguageHost {
|
|
|
71
84
|
/* Where `project.problems` reads its reports, such as a machine that also reports files no editor here holds; the service's diagnostics without one. */
|
|
72
85
|
readonly problemSource?: ProblemSource;
|
|
73
86
|
openPlace?(place: Place): void;
|
|
87
|
+
/* Opens a web address outside the editor, from a link in the text or a server; a new browser window without it. */
|
|
88
|
+
openExternal?(url: string): void;
|
|
74
89
|
pathOfUri?(uri: string): string | null;
|
|
75
90
|
serverNames?(uri: string): readonly string[];
|
|
76
91
|
notify?(notification: EditorNotification): void;
|
|
92
|
+
/* A message of a server for a person, or a line for its log; without it the messages are not shown. */
|
|
93
|
+
onServerMessage?(message: ServerMessage): void;
|
|
77
94
|
/* Synchronous overrides; undefined delegates to built-ins or an editable unknown-variable placeholder. */
|
|
78
95
|
resolveSnippetVariable?(name: string, context: SnippetVariableContext): string | undefined;
|
|
79
96
|
suggestNames?(request: RenameSuggestionsRequest, signal: AbortSignal): Promise<readonly string[]>;
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { KEY_PRIORITY } from './key-priority.ts';
|
|
|
3
3
|
export { realTimers, type Timers } from './timers.ts';
|
|
4
4
|
export { type SemanticTokensOptions } from './semantic-tokens.ts';
|
|
5
5
|
export { ProjectLanguage, type FileOperationTarget } from './project-language.ts';
|
|
6
|
-
export type { LanguageDocumentHandle } from './project-language.ts';
|
|
6
|
+
export type { AcquireOptions, LanguageDocumentHandle } from './project-language.ts';
|
|
7
7
|
|
|
8
8
|
export type {
|
|
9
9
|
SnippetVariableContext,
|
|
@@ -23,6 +23,10 @@ export type { LanguagePopupsProps } from './LanguagePopups.tsx';
|
|
|
23
23
|
export { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
24
24
|
export { SignatureCard } from './SignatureCard.tsx';
|
|
25
25
|
export { type RenderedDocsFeature } from './rendered-docs.ts';
|
|
26
|
+
export { type DocumentLinksFeature } from './document-links.ts';
|
|
27
|
+
export { type DocumentColorsFeature } from './document-colors.ts';
|
|
28
|
+
export { type LinkedEditingFeature } from './linked-editing.ts';
|
|
29
|
+
export { type InlineCompletionFeature, type InlineSuggestion } from './inline-completion.ts';
|
|
26
30
|
export type { RenderedDocEntry, RenderedDocsSettings } from './rendered-docs.ts';
|
|
27
31
|
export type { DocTagEntry, DocTagSection, ParsedDocComment } from './doc-comment-model.ts';
|
|
28
32
|
export { EditorRenderingProvider } from './rendering.tsx';
|
|
@@ -58,14 +62,35 @@ export type {
|
|
|
58
62
|
PickPreview,
|
|
59
63
|
SymbolsView,
|
|
60
64
|
MenuView,
|
|
61
|
-
AuthorsView
|
|
65
|
+
AuthorsView,
|
|
66
|
+
WrapView
|
|
62
67
|
} from './popups.ts';
|
|
63
|
-
|
|
68
|
+
export { type MarkupFeature } from './markup.ts';
|
|
69
|
+
export { type SurroundFeature } from './surround.ts';
|
|
70
|
+
export type { MarkupSettings } from './markup.ts';
|
|
64
71
|
export { RowHost, LineActionHost } from './row-host.ts';
|
|
65
72
|
export type { HostedRow, HostedAction } from './row-host.ts';
|
|
66
73
|
export { type HighlightLayers, highlightLayers } from './highlight-layers.ts';
|
|
67
74
|
export { type NavigationHistory } from './navigation-history.ts';
|
|
68
75
|
export type { Place } from './navigation-history.ts';
|
|
76
|
+
export { EditLocations, type EditPlace } from './edit-locations.ts';
|
|
77
|
+
export { ClipboardHistory, type ClipboardEntry, type ClipboardHistoryOptions } from './clipboard-history.ts';
|
|
78
|
+
export { type PasteHistoryFeature } from './paste-history.ts';
|
|
79
|
+
export {
|
|
80
|
+
TemplateRegistry,
|
|
81
|
+
checkTemplate,
|
|
82
|
+
BUILT_IN_LIVE_TEMPLATES,
|
|
83
|
+
BUILT_IN_POSTFIX_TEMPLATES,
|
|
84
|
+
type TemplatesFeature,
|
|
85
|
+
type LiveTemplate,
|
|
86
|
+
type PostfixTemplate,
|
|
87
|
+
type LiveTemplateDraft,
|
|
88
|
+
type TemplateLanguage,
|
|
89
|
+
type TemplatePlace,
|
|
90
|
+
type TemplateSettings,
|
|
91
|
+
type TemplateRegistryOptions,
|
|
92
|
+
type TemplateUpdate
|
|
93
|
+
} from './templates.ts';
|
|
69
94
|
export { openingPlace, viewStates, followViewStates, viewStateKey, forgetMovesFrom } from './view-state.ts';
|
|
70
95
|
export type { ViewState, RevealLineRequest } from './view-state.ts';
|
|
71
96
|
export { lspLanguageIdOf, shikiLanguageOf, shikiLanguageOfPath } from './language-ids.ts';
|
package/src/inlay-hints.ts
CHANGED
|
@@ -391,7 +391,7 @@ export class InlayHintsFeature {
|
|
|
391
391
|
|
|
392
392
|
/* Whether asking the server could still change what the hint offers. */
|
|
393
393
|
private resolvable(entry: Entry): boolean {
|
|
394
|
-
return !entry.answered && entry.epoch === this.epoch && this.language.
|
|
394
|
+
return !entry.answered && entry.epoch === this.epoch && this.language.service.supports(RESOLVE_METHOD, this.language.uri);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
/* Underlines the part under the pointer when a press would do something with it: a command always, a place while the key is down. */
|
|
@@ -479,7 +479,7 @@ export class InlayHintsFeature {
|
|
|
479
479
|
this.cancelHover();
|
|
480
480
|
this.language.goTo(action.location);
|
|
481
481
|
} else if (action.kind === 'run') {
|
|
482
|
-
await
|
|
482
|
+
await this.language.service.executeCommand(uri, action.command);
|
|
483
483
|
} else if (entry.epoch === this.epoch && editsFit(editor, action.edits)) {
|
|
484
484
|
this.cancelHover();
|
|
485
485
|
const applied = editor.applyEdits(action.edits.map((edit) => ({ range: edit.range, text: edit.newText })));
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { EditorPosition, EditorTextChange } from '@adecore/editor';
|
|
2
|
+
import type { InlineCompletionItem, InlineCompletionResult, Range } from '@adecore/lsp';
|
|
3
|
+
import { comparePositions } from './diagnostics-model.ts';
|
|
4
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
6
|
+
import { isShortcut } from './shortcut-keys.ts';
|
|
7
|
+
import { parseSnippet } from './snippet.ts';
|
|
8
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
9
|
+
|
|
10
|
+
const METHOD = 'textDocument/inlineCompletion';
|
|
11
|
+
/* How long typing rests before a suggestion is asked. */
|
|
12
|
+
const DELAY_MS = 300;
|
|
13
|
+
|
|
14
|
+
/* A suggestion as the text it inserts at the caret: what follows what was already typed of it. */
|
|
15
|
+
export interface InlineSuggestion {
|
|
16
|
+
readonly item: InlineCompletionItem;
|
|
17
|
+
/* What replaces `range` on acceptance. */
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly range: Range;
|
|
20
|
+
/* What the ghost shows after the caret. */
|
|
21
|
+
readonly rest: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function itemsOf(result: InlineCompletionResult): InlineCompletionItem[] {
|
|
25
|
+
if (result === null) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return Array.isArray(result) ? result : result.items;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* A server's item as a suggestion at the caret, or null when it does not continue what the line holds: the text the
|
|
33
|
+
* item replaces up to the caret must start its insert text, so the rest can stand after the caret.
|
|
34
|
+
*/
|
|
35
|
+
export function suggestionOf(item: InlineCompletionItem, caret: EditorPosition, textOf: (range: Range) => string): InlineSuggestion | null {
|
|
36
|
+
const text = typeof item.insertText === 'string' ? item.insertText : parseSnippet(item.insertText.value).text;
|
|
37
|
+
const range = item.range ?? { start: caret, end: caret };
|
|
38
|
+
if (comparePositions(range.start, caret) > 0 || comparePositions(range.end, caret) < 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const typed = textOf({ start: range.start, end: caret });
|
|
42
|
+
if (!text.startsWith(typed)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const rest = text.slice(typed.length);
|
|
46
|
+
return rest === '' ? null : { item, text, range, rest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Suggestions a server writes ahead of the caret, drawn as ghost text: asked a moment after typing stops, or at
|
|
51
|
+
* once with the `suggestInline` key of the host. Tab takes the suggestion, Alt+] and Alt+[ step through the others,
|
|
52
|
+
* Escape and any key that moves away put it away. Off unless the host turns it on, since a host may draw
|
|
53
|
+
* suggestions of its own in the same place.
|
|
54
|
+
*/
|
|
55
|
+
export class InlineCompletionFeature {
|
|
56
|
+
private readonly language: EditorLanguage;
|
|
57
|
+
private readonly timers: Timers;
|
|
58
|
+
private enabled = false;
|
|
59
|
+
private suggestions: InlineSuggestion[] = [];
|
|
60
|
+
private index = 0;
|
|
61
|
+
private timer: unknown = null;
|
|
62
|
+
private controller: AbortController | null = null;
|
|
63
|
+
private revision = 0;
|
|
64
|
+
private accepting = false;
|
|
65
|
+
|
|
66
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
67
|
+
this.language = language;
|
|
68
|
+
this.timers = timers;
|
|
69
|
+
const { editor } = language;
|
|
70
|
+
const offs = [
|
|
71
|
+
language.onKey((event) => this.key(event), KEY_PRIORITY.inlineCompletion),
|
|
72
|
+
editor.onTextChange((change) => this.edited(change)),
|
|
73
|
+
editor.onCaret(() => queueMicrotask(() => this.moved()))
|
|
74
|
+
];
|
|
75
|
+
language.onDispose(() => {
|
|
76
|
+
this.hide();
|
|
77
|
+
for (const off of offs) {
|
|
78
|
+
off();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get supported(): boolean {
|
|
84
|
+
const { service } = this.language;
|
|
85
|
+
return service.inlineCompletions !== undefined && service.supports(METHOD, this.language.uri);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get isEnabled(): boolean {
|
|
89
|
+
return this.enabled;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* The suggestion drawn now, or null. */
|
|
93
|
+
get current(): InlineSuggestion | null {
|
|
94
|
+
return this.suggestions[this.index] ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
configure(settings: { readonly enabled?: boolean }): void {
|
|
98
|
+
this.enabled = settings.enabled === true;
|
|
99
|
+
if (!this.enabled) {
|
|
100
|
+
this.hide();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Asks for suggestions at the caret now, as a person asking for one does. */
|
|
105
|
+
trigger(): void {
|
|
106
|
+
this.timers.clear(this.timer);
|
|
107
|
+
void this.ask(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Takes the drawn suggestion into the text; false when there is none. */
|
|
111
|
+
accept(): boolean {
|
|
112
|
+
const suggestion = this.current;
|
|
113
|
+
if (suggestion === null) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const { editor, service, uri } = this.language;
|
|
117
|
+
this.accepting = true;
|
|
118
|
+
try {
|
|
119
|
+
editor.applyEdits([{ range: suggestion.range, text: suggestion.text }]);
|
|
120
|
+
} finally {
|
|
121
|
+
this.accepting = false;
|
|
122
|
+
}
|
|
123
|
+
this.hide();
|
|
124
|
+
const { command } = suggestion.item;
|
|
125
|
+
if (command !== undefined && service.supports('workspace/executeCommand', uri)) {
|
|
126
|
+
void service.executeCommand(uri, command).catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Steps to the next or the previous suggestion. */
|
|
132
|
+
cycle(step: 1 | -1): void {
|
|
133
|
+
if (this.suggestions.length > 1) {
|
|
134
|
+
this.index = (this.index + step + this.suggestions.length) % this.suggestions.length;
|
|
135
|
+
this.draw();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
hide(): void {
|
|
140
|
+
this.revision++;
|
|
141
|
+
this.timers.clear(this.timer);
|
|
142
|
+
this.controller?.abort();
|
|
143
|
+
if (this.suggestions.length > 0) {
|
|
144
|
+
this.suggestions = [];
|
|
145
|
+
this.language.editor.setGhostText(null);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private key(event: KeyboardEvent): boolean {
|
|
150
|
+
if (isShortcut(this.language.shortcuts.suggestInline, event) && this.enabled) {
|
|
151
|
+
this.trigger();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (this.current === null || event.isComposing) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
158
|
+
return this.accept();
|
|
159
|
+
}
|
|
160
|
+
if (event.key === 'Escape') {
|
|
161
|
+
this.hide();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (event.altKey && !event.ctrlKey && !event.metaKey && (event.code === 'BracketRight' || event.code === 'BracketLeft')) {
|
|
165
|
+
this.cycle(event.code === 'BracketRight' ? 1 : -1);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private edited(change: EditorTextChange): void {
|
|
172
|
+
if (this.accepting) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.hide();
|
|
176
|
+
if (this.enabled && change.source === 'input' && this.supported) {
|
|
177
|
+
this.timer = this.timers.set(() => void this.ask(2), DELAY_MS);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* A caret that left the place of the suggestion puts it away. */
|
|
182
|
+
private moved(): void {
|
|
183
|
+
const suggestion = this.current;
|
|
184
|
+
if (suggestion === null) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const caret = this.language.editor.getCaret();
|
|
188
|
+
const selections = this.language.editor.getSelections();
|
|
189
|
+
if (selections.length !== 1 || suggestionOf(suggestion.item, caret, (range) => this.language.editor.textInRange(range)) === null) {
|
|
190
|
+
this.hide();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async ask(triggerKind: 1 | 2): Promise<void> {
|
|
195
|
+
const { editor, service, uri } = this.language;
|
|
196
|
+
if (!this.enabled || !this.supported || editor.getSelections().length !== 1 || editor.isReadOnly?.() === true) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.controller?.abort();
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
this.controller = controller;
|
|
202
|
+
const revision = this.revision;
|
|
203
|
+
const caret = editor.getCaret();
|
|
204
|
+
try {
|
|
205
|
+
const result = await service.inlineCompletions!(uri, caret, { triggerKind }, { signal: controller.signal });
|
|
206
|
+
if (revision !== this.revision || comparePositions(editor.getCaret(), caret) !== 0) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.suggestions = itemsOf(result).flatMap((item) => suggestionOf(item, caret, (range) => editor.textInRange(range)) ?? []);
|
|
210
|
+
this.index = 0;
|
|
211
|
+
this.draw();
|
|
212
|
+
} catch {
|
|
213
|
+
// A refusal or a failing server is no suggestion; the next pause asks again.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private draw(): void {
|
|
218
|
+
const suggestion = this.current;
|
|
219
|
+
this.language.editor.setGhostText(suggestion === null ? null : { position: this.language.editor.getCaret(), text: suggestion.rest });
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/key-priority.ts
CHANGED
|
@@ -5,11 +5,14 @@ import type { EditorKeyHandler } from '@adecore/editor';
|
|
|
5
5
|
* apart, so a handler of the app can stand between two features; 0, the default, comes after all of them.
|
|
6
6
|
*/
|
|
7
7
|
export const KEY_PRIORITY = {
|
|
8
|
-
pick:
|
|
9
|
-
diagnostics:
|
|
10
|
-
hover:
|
|
11
|
-
completion:
|
|
12
|
-
snippets:
|
|
8
|
+
pick: 1600,
|
|
9
|
+
diagnostics: 1500,
|
|
10
|
+
hover: 1400,
|
|
11
|
+
completion: 1300,
|
|
12
|
+
snippets: 1200,
|
|
13
|
+
// Between snippets and markup, so the spacing of a hundred bends here: Tab has to reach the templates after a running snippet and before Emmet.
|
|
14
|
+
templates: 1150,
|
|
15
|
+
markup: 1100,
|
|
13
16
|
signature: 1000,
|
|
14
17
|
highlights: 900,
|
|
15
18
|
codeActions: 800,
|
|
@@ -19,7 +22,9 @@ export const KEY_PRIORITY = {
|
|
|
19
22
|
callHierarchy: 400,
|
|
20
23
|
symbolPicker: 300,
|
|
21
24
|
history: 200,
|
|
22
|
-
renderedDocs: 100
|
|
25
|
+
renderedDocs: 100,
|
|
26
|
+
pasteHistory: 50,
|
|
27
|
+
inlineCompletion: 25
|
|
23
28
|
} as const;
|
|
24
29
|
|
|
25
30
|
interface Entry {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { EditorTextChange } from '@adecore/editor';
|
|
2
|
+
import type { LinkedEditingRanges } from '@adecore/lsp';
|
|
3
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
4
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
5
|
+
|
|
6
|
+
const METHOD = 'textDocument/linkedEditingRange';
|
|
7
|
+
/* How long the caret rests before the ranges around it are asked. */
|
|
8
|
+
const DELAY_MS = 100;
|
|
9
|
+
|
|
10
|
+
interface Span {
|
|
11
|
+
from: number;
|
|
12
|
+
to: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Session {
|
|
16
|
+
/* Offsets in the current text, in the order the server gave them. */
|
|
17
|
+
spans: Span[];
|
|
18
|
+
pattern: RegExp | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* The server's word pattern, anchored, or null when it gave none or one that does not compile. */
|
|
22
|
+
export function wordPatternOf(ranges: LinkedEditingRanges): RegExp | null {
|
|
23
|
+
if (ranges.wordPattern === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return new RegExp(`^(?:${ranges.wordPattern})$`, 'u');
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Ranges a server says are edited together, such as the names of a start tag and its end tag: a change inside
|
|
35
|
+
* one goes into every other, in the undo step of the key. The ranges are asked once the caret rests, and the link
|
|
36
|
+
* ends when an edit falls outside them, the text stops matching the server's word pattern, or there is more
|
|
37
|
+
* than one caret.
|
|
38
|
+
*/
|
|
39
|
+
export class LinkedEditingFeature {
|
|
40
|
+
private readonly language: EditorLanguage;
|
|
41
|
+
private readonly timers: Timers;
|
|
42
|
+
private session: Session | null = null;
|
|
43
|
+
private timer: unknown = null;
|
|
44
|
+
private controller: AbortController | null = null;
|
|
45
|
+
private revision = 0;
|
|
46
|
+
private mirroring = false;
|
|
47
|
+
private enabled = true;
|
|
48
|
+
|
|
49
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
50
|
+
this.language = language;
|
|
51
|
+
this.timers = timers;
|
|
52
|
+
const { editor } = language;
|
|
53
|
+
const offs = [editor.onTextChange((change) => this.edited(change)), editor.onCaret(() => this.moved())];
|
|
54
|
+
language.onDispose(() => {
|
|
55
|
+
this.stop();
|
|
56
|
+
for (const off of offs) {
|
|
57
|
+
off();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get supported(): boolean {
|
|
63
|
+
const { service } = this.language;
|
|
64
|
+
return this.enabled && service.linkedEditingRanges !== undefined && service.supports(METHOD, this.language.uri);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Whether ranges are linked now. */
|
|
68
|
+
get isActive(): boolean {
|
|
69
|
+
return this.session !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Off, nothing is linked; on unless false. */
|
|
73
|
+
configure(settings: { readonly enabled?: boolean }): void {
|
|
74
|
+
this.enabled = settings.enabled !== false;
|
|
75
|
+
if (!this.enabled) {
|
|
76
|
+
this.stop();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private stop(): void {
|
|
81
|
+
this.session = null;
|
|
82
|
+
this.timers.clear(this.timer);
|
|
83
|
+
this.controller?.abort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private caretOffset(): number | null {
|
|
87
|
+
const { editor } = this.language;
|
|
88
|
+
const selections = editor.getSelections();
|
|
89
|
+
if (selections.length !== 1) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const { start, end } = selections[0]!;
|
|
93
|
+
return start.line === end.line && start.character === end.character ? editor.offsetAt(start) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private moved(): void {
|
|
97
|
+
if (this.mirroring) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// The caret moves before the text change of the same key is told, so the session is checked once both are in.
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
const offset = this.caretOffset();
|
|
103
|
+
if (offset !== null && this.session?.spans.some((span) => span.from <= offset && offset <= span.to)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.stop();
|
|
107
|
+
if (offset !== null && this.supported) {
|
|
108
|
+
this.timer = this.timers.set(() => void this.ask(offset), DELAY_MS);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async ask(offset: number): Promise<void> {
|
|
114
|
+
const { editor, service, uri } = this.language;
|
|
115
|
+
const revision = this.revision;
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
this.controller = controller;
|
|
118
|
+
try {
|
|
119
|
+
const ranges = await service.linkedEditingRanges!(uri, editor.positionAt(offset), { signal: controller.signal });
|
|
120
|
+
if (ranges === null || ranges.ranges.length < 2 || revision !== this.revision || this.caretOffset() !== offset) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.session = {
|
|
124
|
+
spans: ranges.ranges.map((range) => ({ from: editor.offsetAt(range.start), to: editor.offsetAt(range.end) })),
|
|
125
|
+
pattern: wordPatternOf(ranges)
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
// No ranges is no link; the next rest of the caret asks again.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private edited(change: EditorTextChange): void {
|
|
133
|
+
const revision = ++this.revision;
|
|
134
|
+
const session = this.session;
|
|
135
|
+
if (this.mirroring || session === null) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const edit = change.changes[0];
|
|
139
|
+
if (change.source === 'external' || change.changes.length !== 1 || edit === undefined || edit.range.start.line !== edit.range.end.line) {
|
|
140
|
+
this.stop();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const from = this.language.editor.offsetAt(edit.range.start);
|
|
144
|
+
const removed = edit.range.end.character - edit.range.start.character;
|
|
145
|
+
const own = session.spans.findIndex((span) => span.from <= from && from + removed <= span.to);
|
|
146
|
+
if (own === -1) {
|
|
147
|
+
this.stop();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const delta = edit.text.length - removed;
|
|
151
|
+
const edited = session.spans[own]!;
|
|
152
|
+
session.spans = session.spans.map((span, index) =>
|
|
153
|
+
index === own ? { from: span.from, to: span.to + delta } : span.from > edited.from ? { from: span.from + delta, to: span.to + delta } : span
|
|
154
|
+
);
|
|
155
|
+
queueMicrotask(() => this.mirror(own, revision));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private mirror(own: number, revision: number): void {
|
|
159
|
+
const session = this.session;
|
|
160
|
+
if (session === null || revision !== this.revision) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const { editor } = this.language;
|
|
164
|
+
const text = editor.getText();
|
|
165
|
+
const source = session.spans[own]!;
|
|
166
|
+
const value = text.slice(source.from, source.to);
|
|
167
|
+
if (session.pattern !== null && value !== '' && !session.pattern.test(value)) {
|
|
168
|
+
this.stop();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const others = session.spans
|
|
172
|
+
.map((span, index) => ({ span, index }))
|
|
173
|
+
.filter(({ span, index }) => index !== own && text.slice(span.from, span.to) !== value);
|
|
174
|
+
if (others.length === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.mirroring = true;
|
|
178
|
+
try {
|
|
179
|
+
const applied = editor.applyEdits(
|
|
180
|
+
others.map(({ span }) => ({ range: { start: editor.positionAt(span.from), end: editor.positionAt(span.to) }, text: value })),
|
|
181
|
+
{ mergeUndo: true }
|
|
182
|
+
);
|
|
183
|
+
if (!applied) {
|
|
184
|
+
this.stop();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
this.mirroring = false;
|
|
189
|
+
}
|
|
190
|
+
// Each replaced span moves every span after it by what its length changed.
|
|
191
|
+
const changed = others.map(({ span }) => ({ at: span.from, delta: value.length - (span.to - span.from) })).sort((left, right) => left.at - right.at);
|
|
192
|
+
const shift = (offset: number): number => offset + changed.filter((entry) => entry.at < offset).reduce((total, entry) => total + entry.delta, 0);
|
|
193
|
+
session.spans = session.spans.map((span, index) => {
|
|
194
|
+
const from = shift(span.from);
|
|
195
|
+
const replaced = others.some((other) => other.index === index);
|
|
196
|
+
return { from, to: replaced ? from + value.length : shift(span.to) };
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|