@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/completion.ts
CHANGED
|
@@ -91,6 +91,30 @@ function mapCompletionEdits(
|
|
|
91
91
|
return valid ? mapped : null;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/** A row a source of the editor offers beside the language server's. */
|
|
95
|
+
export interface LocalCompletionItem {
|
|
96
|
+
/** The label, kind, detail, documentation, filter text and sort text of the row; its edit range is the typed prefix on the caret's line, ending at the caret. */
|
|
97
|
+
readonly item: CompletionItem;
|
|
98
|
+
|
|
99
|
+
/** Runs instead of the insertion: the source inserts the row itself. */
|
|
100
|
+
accept(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What a source is asked for when the list is requested. */
|
|
104
|
+
export interface CompletionRequest {
|
|
105
|
+
readonly position: EditorPosition;
|
|
106
|
+
|
|
107
|
+
/** Whether the person asked for the list with the keyboard. */
|
|
108
|
+
readonly explicit: boolean;
|
|
109
|
+
readonly triggerCharacter?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Rows that come from the editor itself, such as templates, merged into the list of the language server. */
|
|
113
|
+
export interface CompletionSource {
|
|
114
|
+
/** Synchronous and cheap; asked on every request of the list. */
|
|
115
|
+
items(request: CompletionRequest): readonly LocalCompletionItem[];
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
/*
|
|
95
119
|
* The suggestions that open while a word is typed. A single typed identifier character or a trigger
|
|
96
120
|
* character of the server asks for the list; every character after that only filters the list that
|
|
@@ -129,6 +153,13 @@ export class CompletionFeature {
|
|
|
129
153
|
private acceptSequence = 0;
|
|
130
154
|
private disposed = false;
|
|
131
155
|
private readonly resolved = new Map<CompletionItem, CompletionItem>();
|
|
156
|
+
private readonly sources = new Set<CompletionSource>();
|
|
157
|
+
|
|
158
|
+
/* The rows of the list that a source inserts itself. */
|
|
159
|
+
private locals = new Map<CompletionItem, LocalCompletionItem>();
|
|
160
|
+
|
|
161
|
+
/* Whether the list holds the rows `showItems` gave it and no request refreshes it. */
|
|
162
|
+
private shown = false;
|
|
132
163
|
|
|
133
164
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
134
165
|
this.language = language;
|
|
@@ -169,6 +200,30 @@ export class CompletionFeature {
|
|
|
169
200
|
this.beginRequest({ triggerKind: 1 }, 0);
|
|
170
201
|
}
|
|
171
202
|
|
|
203
|
+
/** Adds rows to every list, also in a document without a language server. Returns the function that takes the source away again. */
|
|
204
|
+
addSource(source: CompletionSource): () => void {
|
|
205
|
+
this.sources.add(source);
|
|
206
|
+
|
|
207
|
+
return () => {
|
|
208
|
+
this.sources.delete(source);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Opens the list with only these rows, the typed prefix starting at `start`, as Insert Live Template does. */
|
|
213
|
+
showItems(items: readonly LocalCompletionItem[], start: EditorPosition): void {
|
|
214
|
+
this.language.pick.close();
|
|
215
|
+
this.close();
|
|
216
|
+
this.shown = true;
|
|
217
|
+
this.locals = new Map(items.map((local) => [local.item, local]));
|
|
218
|
+
this.pool = items.map((local) => local.item);
|
|
219
|
+
this.incomplete = false;
|
|
220
|
+
this.start = start;
|
|
221
|
+
this.active = 0;
|
|
222
|
+
this.touched = false;
|
|
223
|
+
this.server = '';
|
|
224
|
+
this.refilter();
|
|
225
|
+
}
|
|
226
|
+
|
|
172
227
|
move(delta: number): void {
|
|
173
228
|
if (this.ranked.length === 0) {
|
|
174
229
|
return;
|
|
@@ -199,6 +254,16 @@ export class CompletionFeature {
|
|
|
199
254
|
return;
|
|
200
255
|
}
|
|
201
256
|
|
|
257
|
+
const local = this.locals.get(entry.item);
|
|
258
|
+
|
|
259
|
+
if (local !== undefined) {
|
|
260
|
+
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
261
|
+
this.close();
|
|
262
|
+
local.accept();
|
|
263
|
+
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
202
267
|
const { editor, project, uri, snippets } = this.language;
|
|
203
268
|
const acceptSequence = ++this.acceptSequence;
|
|
204
269
|
this.accepting?.abort();
|
|
@@ -236,7 +301,7 @@ export class CompletionFeature {
|
|
|
236
301
|
|
|
237
302
|
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
238
303
|
let item = this.resolved.get(entry.item) ?? entry.item;
|
|
239
|
-
const needsResolve = !this.resolved.has(entry.item) &&
|
|
304
|
+
const needsResolve = !this.resolved.has(entry.item) && this.language.service.providerOptions('completionItem/resolve', uri) !== undefined;
|
|
240
305
|
this.close();
|
|
241
306
|
|
|
242
307
|
if (needsResolve) {
|
|
@@ -522,7 +587,7 @@ export class CompletionFeature {
|
|
|
522
587
|
|
|
523
588
|
/* Parameter info for a call that was opened (or that the server asks for), then the item's own command. */
|
|
524
589
|
private afterInsert(item: CompletionItem, commit: string | undefined, lineBefore: string): void {
|
|
525
|
-
const { editor,
|
|
590
|
+
const { editor, uri, signature } = this.language;
|
|
526
591
|
const { start } = editor.getSelection();
|
|
527
592
|
const insideCall = start.character > 0 && editor.textInRange({ start: { line: start.line, character: start.character - 1 }, end: start }) === '(';
|
|
528
593
|
|
|
@@ -539,7 +604,7 @@ export class CompletionFeature {
|
|
|
539
604
|
if (command.command === PARAMETER_HINTS_COMMAND) {
|
|
540
605
|
signature.trigger();
|
|
541
606
|
} else {
|
|
542
|
-
|
|
607
|
+
this.language.service.executeCommand(uri, command).catch(() => undefined);
|
|
543
608
|
}
|
|
544
609
|
}
|
|
545
610
|
|
|
@@ -549,10 +614,10 @@ export class CompletionFeature {
|
|
|
549
614
|
* suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
|
|
550
615
|
*/
|
|
551
616
|
private async resolveNow(item: CompletionItem, signal: AbortSignal): Promise<CompletionItem> {
|
|
552
|
-
const { editor,
|
|
617
|
+
const { editor, uri } = this.language;
|
|
553
618
|
|
|
554
619
|
try {
|
|
555
|
-
return { ...item, ...(await
|
|
620
|
+
return { ...item, ...(await this.language.service.resolveCompletion(uri, item, { signal })) };
|
|
556
621
|
} catch (error) {
|
|
557
622
|
if (!(error instanceof StaleResultError)) {
|
|
558
623
|
return item;
|
|
@@ -560,11 +625,11 @@ export class CompletionFeature {
|
|
|
560
625
|
}
|
|
561
626
|
|
|
562
627
|
try {
|
|
563
|
-
const fresh = itemsOf(await
|
|
628
|
+
const fresh = itemsOf(await this.language.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find(
|
|
564
629
|
(candidate) => candidate.label === item.label && candidate.kind === item.kind
|
|
565
630
|
);
|
|
566
631
|
|
|
567
|
-
return fresh === undefined ? item : { ...fresh, ...(await
|
|
632
|
+
return fresh === undefined ? item : { ...fresh, ...(await this.language.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
|
|
568
633
|
} catch {
|
|
569
634
|
return item;
|
|
570
635
|
}
|
|
@@ -584,6 +649,8 @@ export class CompletionFeature {
|
|
|
584
649
|
this.activeKey = null;
|
|
585
650
|
this.start = null;
|
|
586
651
|
this.resolved.clear();
|
|
652
|
+
this.locals = new Map();
|
|
653
|
+
this.shown = false;
|
|
587
654
|
|
|
588
655
|
if (this.language.popups.getState().completion !== null) {
|
|
589
656
|
this.language.popups.setState({ completion: null });
|
|
@@ -659,6 +726,17 @@ export class CompletionFeature {
|
|
|
659
726
|
private edited(change: EditorTextChange): void {
|
|
660
727
|
this.textRevision++;
|
|
661
728
|
|
|
729
|
+
// The rows were given, so typing only filters them.
|
|
730
|
+
if (this.shown && this.isOpen) {
|
|
731
|
+
if (change.source === 'input' && change.changes.length === 1 && Array.from(change.changes[0]!.text).every(isIdentifierCharacter)) {
|
|
732
|
+
this.refilter();
|
|
733
|
+
} else {
|
|
734
|
+
this.close();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
662
740
|
if (change.source !== 'input' || change.changes.length !== 1) {
|
|
663
741
|
this.close();
|
|
664
742
|
|
|
@@ -703,15 +781,15 @@ export class CompletionFeature {
|
|
|
703
781
|
}
|
|
704
782
|
|
|
705
783
|
private triggerCharacters(): readonly string[] {
|
|
706
|
-
const {
|
|
784
|
+
const { uri } = this.language;
|
|
707
785
|
|
|
708
|
-
return
|
|
786
|
+
return this.language.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
|
|
709
787
|
}
|
|
710
788
|
|
|
711
789
|
private beginRequest(context: CompletionContext, delay: number): void {
|
|
712
|
-
const {
|
|
790
|
+
const { uri } = this.language;
|
|
713
791
|
|
|
714
|
-
if (!
|
|
792
|
+
if (!this.language.service.supports(METHOD, uri) && this.sources.size === 0) {
|
|
715
793
|
return;
|
|
716
794
|
}
|
|
717
795
|
|
|
@@ -726,26 +804,41 @@ export class CompletionFeature {
|
|
|
726
804
|
this.controller?.abort();
|
|
727
805
|
const controller = new AbortController();
|
|
728
806
|
this.controller = controller;
|
|
729
|
-
let normalized: ReturnType<typeof itemsOf
|
|
807
|
+
let normalized: ReturnType<typeof itemsOf> = { items: [], incomplete: false };
|
|
808
|
+
let failed = false;
|
|
730
809
|
|
|
731
|
-
|
|
732
|
-
|
|
810
|
+
if (this.language.service.supports(METHOD, uri)) {
|
|
811
|
+
try {
|
|
812
|
+
const result = await this.language.service.completion(uri, position, context, { signal: controller.signal });
|
|
733
813
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
814
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
737
817
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
818
|
+
normalized = itemsOf(result);
|
|
819
|
+
} catch {
|
|
820
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
failed = true;
|
|
742
825
|
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const locals = [...this.sources].flatMap((source) =>
|
|
829
|
+
source.items({ position, explicit: this.explicit && !refresh, triggerCharacter: context.triggerCharacter })
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
// A failed request still leaves the rows of the sources.
|
|
833
|
+
if (failed && locals.length === 0) {
|
|
834
|
+
this.close();
|
|
743
835
|
|
|
744
836
|
return;
|
|
745
837
|
}
|
|
746
838
|
|
|
747
839
|
const { items, incomplete } = normalized;
|
|
748
|
-
this.pool = items;
|
|
840
|
+
this.pool = [...items, ...locals.map((local) => local.item)];
|
|
841
|
+
this.locals = new Map(locals.map((local) => [local.item, local]));
|
|
749
842
|
this.incomplete = incomplete;
|
|
750
843
|
this.resolved.clear();
|
|
751
844
|
this.server = project.host.serverNames?.(uri)[0] ?? '';
|
|
@@ -839,12 +932,17 @@ export class CompletionFeature {
|
|
|
839
932
|
}
|
|
840
933
|
|
|
841
934
|
private scheduleResolve(): void {
|
|
842
|
-
const {
|
|
935
|
+
const { uri } = this.language;
|
|
843
936
|
this.timers.clear(this.resolveTimer);
|
|
844
937
|
this.resolving?.abort();
|
|
845
938
|
const entry = this.ranked[this.active];
|
|
846
939
|
|
|
847
|
-
if (
|
|
940
|
+
if (
|
|
941
|
+
entry === undefined ||
|
|
942
|
+
this.locals.has(entry.item) ||
|
|
943
|
+
this.resolved.has(entry.item) ||
|
|
944
|
+
this.language.service.providerOptions('completionItem/resolve', uri) === undefined
|
|
945
|
+
) {
|
|
848
946
|
return;
|
|
849
947
|
}
|
|
850
948
|
|
|
@@ -854,7 +952,7 @@ export class CompletionFeature {
|
|
|
854
952
|
const controller = new AbortController();
|
|
855
953
|
this.resolving = controller;
|
|
856
954
|
|
|
857
|
-
void
|
|
955
|
+
void this.language.service
|
|
858
956
|
.resolveCompletion(uri, entry.item, { signal: controller.signal })
|
|
859
957
|
.then((item) => {
|
|
860
958
|
this.resolved.set(entry.item, { ...entry.item, ...item });
|
package/src/context-menu-rows.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KeymapId } from '@adecore/editor/keymap';
|
|
2
2
|
import type { NavigationKind } from './navigation.ts';
|
|
3
3
|
|
|
4
|
-
export type MenuRowId = NavigationKind | 'peekReferences' | 'callHierarchy' | 'renameSymbol' | 'codeActions';
|
|
4
|
+
export type MenuRowId = NavigationKind | 'peekReferences' | 'callHierarchy' | 'typeHierarchy' | 'renameSymbol' | 'codeActions';
|
|
5
5
|
|
|
6
6
|
export interface MenuRowSpec {
|
|
7
7
|
readonly id: MenuRowId;
|
|
@@ -19,7 +19,8 @@ const GO_TO: readonly MenuRowSpec[] = [
|
|
|
19
19
|
|
|
20
20
|
const REFERENCES: readonly MenuRowSpec[] = [
|
|
21
21
|
{ id: 'peekReferences', method: 'textDocument/references', shortcut: 'peekReferences' },
|
|
22
|
-
{ id: 'callHierarchy', method: 'textDocument/prepareCallHierarchy', shortcut: 'callHierarchy' }
|
|
22
|
+
{ id: 'callHierarchy', method: 'textDocument/prepareCallHierarchy', shortcut: 'callHierarchy' },
|
|
23
|
+
{ id: 'typeHierarchy', method: 'textDocument/prepareTypeHierarchy', shortcut: 'typeHierarchy' }
|
|
23
24
|
];
|
|
24
25
|
|
|
25
26
|
const CHANGES: readonly MenuRowSpec[] = [
|
package/src/definition-link.ts
CHANGED
|
@@ -34,8 +34,8 @@ export function followModKey(apple: boolean, onHeld: (held: boolean) => void): (
|
|
|
34
34
|
|
|
35
35
|
/*
|
|
36
36
|
* The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
|
|
37
|
-
* is defined, which is what a Mod+click on it follows.
|
|
38
|
-
* key comes from the window.
|
|
37
|
+
* is defined, which is what a Mod+click on it follows. A document link under the pointer goes first. The
|
|
38
|
+
* editor only says where the pointer is, so the key comes from the window.
|
|
39
39
|
*/
|
|
40
40
|
export class DefinitionLinkFeature {
|
|
41
41
|
private readonly language: EditorLanguage;
|
|
@@ -90,13 +90,25 @@ export class DefinitionLinkFeature {
|
|
|
90
90
|
private update(): void {
|
|
91
91
|
const { pointer } = this;
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
const link = pointer === null ? null : this.language.links.rangeAt(pointer);
|
|
94
|
+
if (this.held && link !== null) {
|
|
95
|
+
const key = `link:${link.start.line}:${link.start.character}`;
|
|
96
|
+
if (key !== this.shownKey) {
|
|
97
|
+
this.clear();
|
|
98
|
+
this.shownKey = key;
|
|
99
|
+
this.drawn = true;
|
|
100
|
+
this.language.editor.setLink(link);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!this.held || pointer === null || !this.language.service.supports('textDocument/definition', this.language.uri)) {
|
|
94
106
|
this.clear();
|
|
95
107
|
|
|
96
108
|
return;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
const { editor,
|
|
111
|
+
const { editor, uri } = this.language;
|
|
100
112
|
const line = lineTextOf(editor, pointer.line);
|
|
101
113
|
const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
|
|
102
114
|
|
|
@@ -118,7 +130,7 @@ export class DefinitionLinkFeature {
|
|
|
118
130
|
const controller = new AbortController();
|
|
119
131
|
this.lookup = controller;
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
this.language.service
|
|
122
134
|
.definition(uri, pointer, { signal: controller.signal })
|
|
123
135
|
.then((result) => {
|
|
124
136
|
if (request === this.request && locationsOf(result).length > 0) {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { EditorContentChange, EditorInlayClick, EditorInlayHint } from '@adecore/editor';
|
|
2
|
+
import type { Color, ColorInformation, ColorPresentation, TextEdit } from '@adecore/lsp';
|
|
3
|
+
import { DocumentQuery } from './document-query.ts';
|
|
4
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
6
|
+
|
|
7
|
+
const METHOD = 'textDocument/documentColor';
|
|
8
|
+
const OWNER = 'document-colors';
|
|
9
|
+
const DELAY_MS = 500;
|
|
10
|
+
const ID_PREFIX = 'color-';
|
|
11
|
+
const OTHER_COLOR = 'other';
|
|
12
|
+
|
|
13
|
+
/* A color of LSP, each part from 0 to 1, as CSS draws it. */
|
|
14
|
+
export function cssColorOf(color: Color): string {
|
|
15
|
+
const part = (value: number): string => String(Math.round(Math.min(1, Math.max(0, value)) * 1000) / 1000);
|
|
16
|
+
return `color(srgb ${part(color.red)} ${part(color.green)} ${part(color.blue)} / ${part(color.alpha)})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* `#rrggbb` of an `<input type="color">` as a color of LSP, opaque. */
|
|
20
|
+
export function colorOfHex(value: string): Color | null {
|
|
21
|
+
const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
|
|
22
|
+
if (match === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return { red: parseInt(match[1]!, 16) / 255, green: parseInt(match[2]!, 16) / 255, blue: parseInt(match[3]!, 16) / 255, alpha: 1 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hexOf(color: Color): string {
|
|
29
|
+
const part = (value: number): string =>
|
|
30
|
+
Math.round(Math.min(1, Math.max(0, value)) * 255)
|
|
31
|
+
.toString(16)
|
|
32
|
+
.padStart(2, '0');
|
|
33
|
+
return `#${part(color.red)}${part(color.green)}${part(color.blue)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The edits a presentation makes: its own text edit, or its label over the color, and the edits that come with it. */
|
|
37
|
+
export function presentationEdits(presentation: ColorPresentation, information: ColorInformation): TextEdit[] {
|
|
38
|
+
return [presentation.textEdit ?? { range: information.range, newText: presentation.label }, ...(presentation.additionalTextEdits ?? [])];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* The colors a server finds in the text, such as `#ff0000` in a stylesheet, each with a swatch before it. A press on
|
|
43
|
+
* a swatch lists the ways the server writes that color, so `rgb()` can become `#hex`, and offers a color of the
|
|
44
|
+
* platform's picker, which keeps the way the color was written when the server offers it.
|
|
45
|
+
*/
|
|
46
|
+
export class DocumentColorsFeature {
|
|
47
|
+
private readonly language: EditorLanguage;
|
|
48
|
+
private colors: readonly ColorInformation[] = [];
|
|
49
|
+
private readonly query: DocumentQuery<ColorInformation[]>;
|
|
50
|
+
private enabled = true;
|
|
51
|
+
|
|
52
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
53
|
+
this.language = language;
|
|
54
|
+
this.query = new DocumentQuery(
|
|
55
|
+
language,
|
|
56
|
+
timers,
|
|
57
|
+
DELAY_MS,
|
|
58
|
+
(signal) => (this.enabled && this.supported ? language.service.documentColors!(language.uri, { signal }) : Promise.resolve(null)),
|
|
59
|
+
(colors) => this.show(colors ?? [])
|
|
60
|
+
);
|
|
61
|
+
const { editor } = language;
|
|
62
|
+
if (editor.onInlayClick !== undefined) {
|
|
63
|
+
language.onDispose(editor.onInlayClick((click) => this.pressed(click)));
|
|
64
|
+
}
|
|
65
|
+
language.onDispose(() => editor.setInlayHints([], OWNER));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get supported(): boolean {
|
|
69
|
+
const { service } = this.language;
|
|
70
|
+
return service.documentColors !== undefined && service.supports(METHOD, this.language.uri);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The colors of the current text, empty while an edit waits for the next answer. */
|
|
74
|
+
all(): readonly ColorInformation[] {
|
|
75
|
+
return this.colors;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Off, there are no swatches; on unless false. */
|
|
79
|
+
configure(settings: { readonly enabled?: boolean }): void {
|
|
80
|
+
this.enabled = settings.enabled !== false;
|
|
81
|
+
if (this.enabled) {
|
|
82
|
+
this.query.run();
|
|
83
|
+
} else {
|
|
84
|
+
this.show([]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* The ways the server writes a color found in the text, for a menu of the host. */
|
|
89
|
+
async presentations(information: ColorInformation, color: Color = information.color): Promise<ColorPresentation[]> {
|
|
90
|
+
const { service } = this.language;
|
|
91
|
+
if (service.colorPresentations === undefined) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return (await service.colorPresentations(this.language.uri, color, information.range)) ?? [];
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Writes a presentation in place of the color it was asked for; false when the text moved on since. */
|
|
102
|
+
apply(presentation: ColorPresentation, information: ColorInformation): boolean {
|
|
103
|
+
if (!this.colors.includes(information)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const changes: EditorContentChange[] = presentationEdits(presentation, information).map((edit) => ({ range: edit.range, text: edit.newText }));
|
|
107
|
+
return this.language.editor.applyEdits(changes);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private show(colors: readonly ColorInformation[]): void {
|
|
111
|
+
this.colors = colors;
|
|
112
|
+
const hints: EditorInlayHint[] = colors.map((information, index) => ({
|
|
113
|
+
id: `${ID_PREFIX}${index}`,
|
|
114
|
+
position: information.range.start,
|
|
115
|
+
label: this.language.project.i18n.t('editor:language.colors.swatch', { color: hexOf(information.color) }),
|
|
116
|
+
swatch: cssColorOf(information.color),
|
|
117
|
+
paddingRight: true
|
|
118
|
+
}));
|
|
119
|
+
this.language.editor.setInlayHints(hints, OWNER);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private pressed(click: EditorInlayClick): boolean {
|
|
123
|
+
if (!click.id.startsWith(ID_PREFIX)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const information = this.colors[Number(click.id.slice(ID_PREFIX.length))];
|
|
127
|
+
if (information === undefined) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
void this.open(information);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async open(information: ColorInformation): Promise<void> {
|
|
135
|
+
const presentations = await this.presentations(information);
|
|
136
|
+
if (!this.colors.includes(information)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { pick, project } = this.language;
|
|
140
|
+
const rows = presentations.map((presentation, index) => ({ id: String(index), label: presentation.label, detail: '' }));
|
|
141
|
+
pick.open({
|
|
142
|
+
anchor: information.range.start,
|
|
143
|
+
title: project.i18n.t('editor:language.colors.title'),
|
|
144
|
+
groups: [
|
|
145
|
+
{ title: null, rows },
|
|
146
|
+
{ title: null, rows: [{ id: OTHER_COLOR, label: project.i18n.t('editor:language.colors.other'), detail: '' }] }
|
|
147
|
+
],
|
|
148
|
+
accept: (id) => {
|
|
149
|
+
if (id === OTHER_COLOR) {
|
|
150
|
+
this.choose(information, presentations);
|
|
151
|
+
} else {
|
|
152
|
+
this.apply(presentations[Number(id)]!, information);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* The platform's color picker; the new color is written the way the old one was when the server offers that. */
|
|
159
|
+
private choose(information: ColorInformation, before: readonly ColorPresentation[]): void {
|
|
160
|
+
if (typeof document === 'undefined') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const written = this.language.editor.textInRange(information.range);
|
|
164
|
+
const form = Math.max(
|
|
165
|
+
0,
|
|
166
|
+
before.findIndex((presentation) => presentation.label === written)
|
|
167
|
+
);
|
|
168
|
+
const input = document.createElement('input');
|
|
169
|
+
input.type = 'color';
|
|
170
|
+
input.value = hexOf(information.color);
|
|
171
|
+
input.addEventListener(
|
|
172
|
+
'change',
|
|
173
|
+
() => {
|
|
174
|
+
const color = colorOfHex(input.value);
|
|
175
|
+
if (color === null) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
void this.presentations(information, { ...color, alpha: information.color.alpha }).then((presentations) => {
|
|
179
|
+
const presentation = presentations[form] ?? presentations[0];
|
|
180
|
+
if (presentation !== undefined) {
|
|
181
|
+
this.apply(presentation, information);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
{ once: true }
|
|
186
|
+
);
|
|
187
|
+
input.click();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
+
import type { DocumentLink } from '@adecore/lsp';
|
|
3
|
+
import { comparePositions } from './diagnostics-model.ts';
|
|
4
|
+
import { DocumentQuery } from './document-query.ts';
|
|
5
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
6
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
7
|
+
|
|
8
|
+
const METHOD = 'textDocument/documentLink';
|
|
9
|
+
const RESOLVE = 'documentLink/resolve';
|
|
10
|
+
/* How long after the last edit the links are asked again. */
|
|
11
|
+
const DELAY_MS = 500;
|
|
12
|
+
/* `#L12`, `#12` or `#L12,4`: a line, from one, and a column, from one, the form editors write into a link. */
|
|
13
|
+
const LINE_FRAGMENT = /^#L?(\d+)(?:[,:C](\d+))?$/i;
|
|
14
|
+
|
|
15
|
+
/* Where a link to a file of the editor lands, from the line its fragment names; the start of the file without one. */
|
|
16
|
+
export function linkPosition(target: string): { readonly uri: string; readonly position: EditorPosition } {
|
|
17
|
+
const hash = target.indexOf('#');
|
|
18
|
+
const fragment = hash === -1 ? null : LINE_FRAGMENT.exec(target.slice(hash));
|
|
19
|
+
if (fragment === null) {
|
|
20
|
+
return { uri: target, position: { line: 0, character: 0 } };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
uri: target.slice(0, hash),
|
|
24
|
+
position: { line: Math.max(0, Number(fragment[1]) - 1), character: Math.max(0, Number(fragment[2] ?? 1) - 1) }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Whether a target opens in the editor: a file, or a document of a scheme a server serves. A web address opens outside. */
|
|
29
|
+
function opensInEditor(target: string): boolean {
|
|
30
|
+
return !/^(https?|mailto|ftp):/i.test(target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* The links a server finds in the text, such as an import path or a web address in a comment. Mod+click on one
|
|
35
|
+
* follows it, and while Mod is held the link under the pointer is drawn as one (`DefinitionLinkFeature` draws it).
|
|
36
|
+
* A link without a target asks the server for it first.
|
|
37
|
+
*/
|
|
38
|
+
export class DocumentLinksFeature {
|
|
39
|
+
private readonly language: EditorLanguage;
|
|
40
|
+
private links: readonly DocumentLink[] = [];
|
|
41
|
+
private readonly query: DocumentQuery<DocumentLink[]>;
|
|
42
|
+
|
|
43
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
44
|
+
this.language = language;
|
|
45
|
+
this.query = new DocumentQuery(
|
|
46
|
+
language,
|
|
47
|
+
timers,
|
|
48
|
+
DELAY_MS,
|
|
49
|
+
(signal) => (this.supported ? language.service.documentLinks!(language.uri, { signal }) : Promise.resolve(null)),
|
|
50
|
+
(links) => {
|
|
51
|
+
this.links = links ?? [];
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
const off = language.editor.onClick((click) => {
|
|
55
|
+
if (!click.mod || click.alt || click.shift) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const link = this.linkAt(click.position);
|
|
59
|
+
if (link === null) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
void this.follow(link);
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
language.onDispose(off);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get supported(): boolean {
|
|
69
|
+
const { service } = this.language;
|
|
70
|
+
return service.documentLinks !== undefined && service.supports(METHOD, this.language.uri);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The links of the current text, empty while an edit waits for the next answer. */
|
|
74
|
+
all(): readonly DocumentLink[] {
|
|
75
|
+
return this.links;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The link whose range holds `position`, its end left out. */
|
|
79
|
+
linkAt(position: EditorPosition): DocumentLink | null {
|
|
80
|
+
return this.links.find((link) => comparePositions(link.range.start, position) <= 0 && comparePositions(position, link.range.end) < 0) ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* The range of the link at `position`, which the pointer draws as a link while Mod is held. */
|
|
84
|
+
rangeAt(position: EditorPosition): EditorRange | null {
|
|
85
|
+
return this.linkAt(position)?.range ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
refresh(): void {
|
|
89
|
+
this.query.run();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Opens what a link points at; false when it has no target, even after asking. */
|
|
93
|
+
async follow(link: DocumentLink): Promise<boolean> {
|
|
94
|
+
const target = link.target ?? (await this.resolve(link))?.target;
|
|
95
|
+
if (target === undefined) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
openTarget(this.language, target);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async resolve(link: DocumentLink): Promise<DocumentLink | null> {
|
|
103
|
+
const { service } = this.language;
|
|
104
|
+
if (service.resolveDocumentLink === undefined || service.providerOptions(RESOLVE, this.language.uri)?.resolveProvider !== true) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await service.resolveDocumentLink(this.language.uri, link);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Opens a file or a document of a server in the editor, and a web address through the host. */
|
|
116
|
+
export function openTarget(language: EditorLanguage, target: string, selection?: EditorRange): void {
|
|
117
|
+
if (!opensInEditor(target)) {
|
|
118
|
+
const { host } = language.project;
|
|
119
|
+
if (host.openExternal !== undefined) {
|
|
120
|
+
host.openExternal(target);
|
|
121
|
+
} else if (typeof window !== 'undefined') {
|
|
122
|
+
window.open(target, '_blank', 'noopener,noreferrer');
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const { uri, position } = linkPosition(target);
|
|
127
|
+
const range = selection ?? { start: position, end: position };
|
|
128
|
+
language.goTo({ uri, range });
|
|
129
|
+
}
|