@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/dist/completion.js
CHANGED
|
@@ -101,6 +101,11 @@ export class CompletionFeature {
|
|
|
101
101
|
acceptSequence = 0;
|
|
102
102
|
disposed = false;
|
|
103
103
|
resolved = new Map();
|
|
104
|
+
sources = new Set();
|
|
105
|
+
/* The rows of the list that a source inserts itself. */
|
|
106
|
+
locals = new Map();
|
|
107
|
+
/* Whether the list holds the rows `showItems` gave it and no request refreshes it. */
|
|
108
|
+
shown = false;
|
|
104
109
|
constructor(language, timers = realTimers) {
|
|
105
110
|
this.language = language;
|
|
106
111
|
this.timers = timers;
|
|
@@ -132,6 +137,27 @@ export class CompletionFeature {
|
|
|
132
137
|
triggerKind: 1
|
|
133
138
|
}, 0);
|
|
134
139
|
}
|
|
140
|
+
/** Adds rows to every list, also in a document without a language server. Returns the function that takes the source away again. */
|
|
141
|
+
addSource(source) {
|
|
142
|
+
this.sources.add(source);
|
|
143
|
+
return () => {
|
|
144
|
+
this.sources.delete(source);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Opens the list with only these rows, the typed prefix starting at `start`, as Insert Live Template does. */
|
|
148
|
+
showItems(items, start) {
|
|
149
|
+
this.language.pick.close();
|
|
150
|
+
this.close();
|
|
151
|
+
this.shown = true;
|
|
152
|
+
this.locals = new Map(items.map(local => [local.item, local]));
|
|
153
|
+
this.pool = items.map(local => local.item);
|
|
154
|
+
this.incomplete = false;
|
|
155
|
+
this.start = start;
|
|
156
|
+
this.active = 0;
|
|
157
|
+
this.touched = false;
|
|
158
|
+
this.server = '';
|
|
159
|
+
this.refilter();
|
|
160
|
+
}
|
|
135
161
|
move(delta) {
|
|
136
162
|
if (this.ranked.length === 0) {
|
|
137
163
|
return;
|
|
@@ -165,6 +191,13 @@ export class CompletionFeature {
|
|
|
165
191
|
if (entry === undefined) {
|
|
166
192
|
return;
|
|
167
193
|
}
|
|
194
|
+
const local = this.locals.get(entry.item);
|
|
195
|
+
if (local !== undefined) {
|
|
196
|
+
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
197
|
+
this.close();
|
|
198
|
+
local.accept();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
168
201
|
const {
|
|
169
202
|
editor,
|
|
170
203
|
project,
|
|
@@ -198,7 +231,7 @@ export class CompletionFeature {
|
|
|
198
231
|
};
|
|
199
232
|
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
200
233
|
let item = this.resolved.get(entry.item) ?? entry.item;
|
|
201
|
-
const needsResolve = !this.resolved.has(entry.item) &&
|
|
234
|
+
const needsResolve = !this.resolved.has(entry.item) && this.language.service.providerOptions('completionItem/resolve', uri) !== undefined;
|
|
202
235
|
this.close();
|
|
203
236
|
if (needsResolve) {
|
|
204
237
|
// Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
|
|
@@ -482,7 +515,6 @@ export class CompletionFeature {
|
|
|
482
515
|
afterInsert(item, commit, lineBefore) {
|
|
483
516
|
const {
|
|
484
517
|
editor,
|
|
485
|
-
project,
|
|
486
518
|
uri,
|
|
487
519
|
signature
|
|
488
520
|
} = this.language;
|
|
@@ -508,7 +540,7 @@ export class CompletionFeature {
|
|
|
508
540
|
if (command.command === PARAMETER_HINTS_COMMAND) {
|
|
509
541
|
signature.trigger();
|
|
510
542
|
} else {
|
|
511
|
-
|
|
543
|
+
this.language.service.executeCommand(uri, command).catch(() => undefined);
|
|
512
544
|
}
|
|
513
545
|
}
|
|
514
546
|
/*
|
|
@@ -519,13 +551,12 @@ export class CompletionFeature {
|
|
|
519
551
|
async resolveNow(item, signal) {
|
|
520
552
|
const {
|
|
521
553
|
editor,
|
|
522
|
-
project,
|
|
523
554
|
uri
|
|
524
555
|
} = this.language;
|
|
525
556
|
try {
|
|
526
557
|
return {
|
|
527
558
|
...item,
|
|
528
|
-
...(await
|
|
559
|
+
...(await this.language.service.resolveCompletion(uri, item, {
|
|
529
560
|
signal
|
|
530
561
|
}))
|
|
531
562
|
};
|
|
@@ -535,14 +566,14 @@ export class CompletionFeature {
|
|
|
535
566
|
}
|
|
536
567
|
}
|
|
537
568
|
try {
|
|
538
|
-
const fresh = itemsOf(await
|
|
569
|
+
const fresh = itemsOf(await this.language.service.completion(uri, editor.getCaret(), {
|
|
539
570
|
triggerKind: 1
|
|
540
571
|
}, {
|
|
541
572
|
signal
|
|
542
573
|
})).items.find(candidate => candidate.label === item.label && candidate.kind === item.kind);
|
|
543
574
|
return fresh === undefined ? item : {
|
|
544
575
|
...fresh,
|
|
545
|
-
...(await
|
|
576
|
+
...(await this.language.service.resolveCompletion(uri, fresh, {
|
|
546
577
|
signal
|
|
547
578
|
}).catch(() => fresh))
|
|
548
579
|
};
|
|
@@ -564,6 +595,8 @@ export class CompletionFeature {
|
|
|
564
595
|
this.activeKey = null;
|
|
565
596
|
this.start = null;
|
|
566
597
|
this.resolved.clear();
|
|
598
|
+
this.locals = new Map();
|
|
599
|
+
this.shown = false;
|
|
567
600
|
if (this.language.popups.getState().completion !== null) {
|
|
568
601
|
this.language.popups.setState({
|
|
569
602
|
completion: null
|
|
@@ -623,6 +656,15 @@ export class CompletionFeature {
|
|
|
623
656
|
}
|
|
624
657
|
edited(change) {
|
|
625
658
|
this.textRevision++;
|
|
659
|
+
// The rows were given, so typing only filters them.
|
|
660
|
+
if (this.shown && this.isOpen) {
|
|
661
|
+
if (change.source === 'input' && change.changes.length === 1 && Array.from(change.changes[0].text).every(isIdentifierCharacter)) {
|
|
662
|
+
this.refilter();
|
|
663
|
+
} else {
|
|
664
|
+
this.close();
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
626
668
|
if (change.source !== 'input' || change.changes.length !== 1) {
|
|
627
669
|
this.close();
|
|
628
670
|
return;
|
|
@@ -672,17 +714,15 @@ export class CompletionFeature {
|
|
|
672
714
|
}
|
|
673
715
|
triggerCharacters() {
|
|
674
716
|
const {
|
|
675
|
-
project,
|
|
676
717
|
uri
|
|
677
718
|
} = this.language;
|
|
678
|
-
return
|
|
719
|
+
return this.language.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
|
|
679
720
|
}
|
|
680
721
|
beginRequest(context, delay) {
|
|
681
722
|
const {
|
|
682
|
-
project,
|
|
683
723
|
uri
|
|
684
724
|
} = this.language;
|
|
685
|
-
if (!
|
|
725
|
+
if (!this.language.service.supports(METHOD, uri) && this.sources.size === 0) {
|
|
686
726
|
return;
|
|
687
727
|
}
|
|
688
728
|
this.timers.clear(this.requestTimer);
|
|
@@ -699,26 +739,43 @@ export class CompletionFeature {
|
|
|
699
739
|
this.controller?.abort();
|
|
700
740
|
const controller = new AbortController();
|
|
701
741
|
this.controller = controller;
|
|
702
|
-
let normalized
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
742
|
+
let normalized = {
|
|
743
|
+
items: [],
|
|
744
|
+
incomplete: false
|
|
745
|
+
};
|
|
746
|
+
let failed = false;
|
|
747
|
+
if (this.language.service.supports(METHOD, uri)) {
|
|
748
|
+
try {
|
|
749
|
+
const result = await this.language.service.completion(uri, position, context, {
|
|
750
|
+
signal: controller.signal
|
|
751
|
+
});
|
|
752
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
normalized = itemsOf(result);
|
|
756
|
+
} catch {
|
|
757
|
+
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
failed = true;
|
|
714
761
|
}
|
|
762
|
+
}
|
|
763
|
+
const locals = [...this.sources].flatMap(source => source.items({
|
|
764
|
+
position,
|
|
765
|
+
explicit: this.explicit && !refresh,
|
|
766
|
+
triggerCharacter: context.triggerCharacter
|
|
767
|
+
}));
|
|
768
|
+
// A failed request still leaves the rows of the sources.
|
|
769
|
+
if (failed && locals.length === 0) {
|
|
770
|
+
this.close();
|
|
715
771
|
return;
|
|
716
772
|
}
|
|
717
773
|
const {
|
|
718
774
|
items,
|
|
719
775
|
incomplete
|
|
720
776
|
} = normalized;
|
|
721
|
-
this.pool = items;
|
|
777
|
+
this.pool = [...items, ...locals.map(local => local.item)];
|
|
778
|
+
this.locals = new Map(locals.map(local => [local.item, local]));
|
|
722
779
|
this.incomplete = incomplete;
|
|
723
780
|
this.resolved.clear();
|
|
724
781
|
this.server = project.host.serverNames?.(uri)[0] ?? '';
|
|
@@ -805,20 +862,19 @@ export class CompletionFeature {
|
|
|
805
862
|
}
|
|
806
863
|
scheduleResolve() {
|
|
807
864
|
const {
|
|
808
|
-
project,
|
|
809
865
|
uri
|
|
810
866
|
} = this.language;
|
|
811
867
|
this.timers.clear(this.resolveTimer);
|
|
812
868
|
this.resolving?.abort();
|
|
813
869
|
const entry = this.ranked[this.active];
|
|
814
|
-
if (entry === undefined || this.resolved.has(entry.item) ||
|
|
870
|
+
if (entry === undefined || this.locals.has(entry.item) || this.resolved.has(entry.item) || this.language.service.providerOptions('completionItem/resolve', uri) === undefined) {
|
|
815
871
|
return;
|
|
816
872
|
}
|
|
817
873
|
const request = this.request;
|
|
818
874
|
this.resolveTimer = this.timers.set(() => {
|
|
819
875
|
const controller = new AbortController();
|
|
820
876
|
this.resolving = controller;
|
|
821
|
-
void
|
|
877
|
+
void this.language.service.resolveCompletion(uri, entry.item, {
|
|
822
878
|
signal: controller.signal
|
|
823
879
|
}).then(item => {
|
|
824
880
|
this.resolved.set(entry.item, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KeymapId } from '@adecore/editor/keymap';
|
|
2
2
|
import type { NavigationKind } from './navigation.ts';
|
|
3
|
-
export type MenuRowId = NavigationKind | 'peekReferences' | 'callHierarchy' | 'renameSymbol' | 'codeActions';
|
|
3
|
+
export type MenuRowId = NavigationKind | 'peekReferences' | 'callHierarchy' | 'typeHierarchy' | 'renameSymbol' | 'codeActions';
|
|
4
4
|
export interface MenuRowSpec {
|
|
5
5
|
readonly id: MenuRowId;
|
|
6
6
|
readonly method: string;
|
|
@@ -24,6 +24,10 @@ const REFERENCES = [{
|
|
|
24
24
|
id: 'callHierarchy',
|
|
25
25
|
method: 'textDocument/prepareCallHierarchy',
|
|
26
26
|
shortcut: 'callHierarchy'
|
|
27
|
+
}, {
|
|
28
|
+
id: 'typeHierarchy',
|
|
29
|
+
method: 'textDocument/prepareTypeHierarchy',
|
|
30
|
+
shortcut: 'typeHierarchy'
|
|
27
31
|
}];
|
|
28
32
|
const CHANGES = [{
|
|
29
33
|
id: 'renameSymbol',
|
package/dist/definition-link.js
CHANGED
|
@@ -25,8 +25,8 @@ export function followModKey(apple, onHeld) {
|
|
|
25
25
|
}
|
|
26
26
|
/*
|
|
27
27
|
* The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
|
|
28
|
-
* is defined, which is what a Mod+click on it follows.
|
|
29
|
-
* key comes from the window.
|
|
28
|
+
* is defined, which is what a Mod+click on it follows. A document link under the pointer goes first. The
|
|
29
|
+
* editor only says where the pointer is, so the key comes from the window.
|
|
30
30
|
*/
|
|
31
31
|
export class DefinitionLinkFeature {
|
|
32
32
|
language;
|
|
@@ -71,13 +71,23 @@ export class DefinitionLinkFeature {
|
|
|
71
71
|
const {
|
|
72
72
|
pointer
|
|
73
73
|
} = this;
|
|
74
|
-
|
|
74
|
+
const link = pointer === null ? null : this.language.links.rangeAt(pointer);
|
|
75
|
+
if (this.held && link !== null) {
|
|
76
|
+
const key = `link:${link.start.line}:${link.start.character}`;
|
|
77
|
+
if (key !== this.shownKey) {
|
|
78
|
+
this.clear();
|
|
79
|
+
this.shownKey = key;
|
|
80
|
+
this.drawn = true;
|
|
81
|
+
this.language.editor.setLink(link);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!this.held || pointer === null || !this.language.service.supports('textDocument/definition', this.language.uri)) {
|
|
75
86
|
this.clear();
|
|
76
87
|
return;
|
|
77
88
|
}
|
|
78
89
|
const {
|
|
79
90
|
editor,
|
|
80
|
-
project,
|
|
81
91
|
uri
|
|
82
92
|
} = this.language;
|
|
83
93
|
const line = lineTextOf(editor, pointer.line);
|
|
@@ -95,7 +105,7 @@ export class DefinitionLinkFeature {
|
|
|
95
105
|
const request = ++this.request;
|
|
96
106
|
const controller = new AbortController();
|
|
97
107
|
this.lookup = controller;
|
|
98
|
-
|
|
108
|
+
this.language.service.definition(uri, pointer, {
|
|
99
109
|
signal: controller.signal
|
|
100
110
|
}).then(result => {
|
|
101
111
|
if (request === this.request && locationsOf(result).length > 0) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Color, ColorInformation, ColorPresentation, TextEdit } from '@adecore/lsp';
|
|
2
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
3
|
+
import { type Timers } from './timers.ts';
|
|
4
|
+
export declare function cssColorOf(color: Color): string;
|
|
5
|
+
export declare function colorOfHex(value: string): Color | null;
|
|
6
|
+
export declare function presentationEdits(presentation: ColorPresentation, information: ColorInformation): TextEdit[];
|
|
7
|
+
export declare class DocumentColorsFeature {
|
|
8
|
+
private readonly language;
|
|
9
|
+
private colors;
|
|
10
|
+
private readonly query;
|
|
11
|
+
private enabled;
|
|
12
|
+
constructor(language: EditorLanguage, timers?: Timers);
|
|
13
|
+
get supported(): boolean;
|
|
14
|
+
all(): readonly ColorInformation[];
|
|
15
|
+
configure(settings: {
|
|
16
|
+
readonly enabled?: boolean;
|
|
17
|
+
}): void;
|
|
18
|
+
presentations(information: ColorInformation, color?: Color): Promise<ColorPresentation[]>;
|
|
19
|
+
apply(presentation: ColorPresentation, information: ColorInformation): boolean;
|
|
20
|
+
private show;
|
|
21
|
+
private pressed;
|
|
22
|
+
private open;
|
|
23
|
+
private choose;
|
|
24
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { DocumentQuery } from './document-query.js';
|
|
2
|
+
import { realTimers } from './timers.js';
|
|
3
|
+
const METHOD = 'textDocument/documentColor';
|
|
4
|
+
const OWNER = 'document-colors';
|
|
5
|
+
const DELAY_MS = 500;
|
|
6
|
+
const ID_PREFIX = 'color-';
|
|
7
|
+
const OTHER_COLOR = 'other';
|
|
8
|
+
/* A color of LSP, each part from 0 to 1, as CSS draws it. */
|
|
9
|
+
export function cssColorOf(color) {
|
|
10
|
+
const part = value => String(Math.round(Math.min(1, Math.max(0, value)) * 1000) / 1000);
|
|
11
|
+
return `color(srgb ${part(color.red)} ${part(color.green)} ${part(color.blue)} / ${part(color.alpha)})`;
|
|
12
|
+
}
|
|
13
|
+
/* `#rrggbb` of an `<input type="color">` as a color of LSP, opaque. */
|
|
14
|
+
export function colorOfHex(value) {
|
|
15
|
+
const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
|
|
16
|
+
if (match === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
red: parseInt(match[1], 16) / 255,
|
|
21
|
+
green: parseInt(match[2], 16) / 255,
|
|
22
|
+
blue: parseInt(match[3], 16) / 255,
|
|
23
|
+
alpha: 1
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function hexOf(color) {
|
|
27
|
+
const part = value => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, '0');
|
|
28
|
+
return `#${part(color.red)}${part(color.green)}${part(color.blue)}`;
|
|
29
|
+
}
|
|
30
|
+
/* The edits a presentation makes: its own text edit, or its label over the color, and the edits that come with it. */
|
|
31
|
+
export function presentationEdits(presentation, information) {
|
|
32
|
+
return [presentation.textEdit ?? {
|
|
33
|
+
range: information.range,
|
|
34
|
+
newText: presentation.label
|
|
35
|
+
}, ...(presentation.additionalTextEdits ?? [])];
|
|
36
|
+
}
|
|
37
|
+
/*
|
|
38
|
+
* The colors a server finds in the text, such as `#ff0000` in a stylesheet, each with a swatch before it. A press on
|
|
39
|
+
* a swatch lists the ways the server writes that color, so `rgb()` can become `#hex`, and offers a color of the
|
|
40
|
+
* platform's picker, which keeps the way the color was written when the server offers it.
|
|
41
|
+
*/
|
|
42
|
+
export class DocumentColorsFeature {
|
|
43
|
+
language;
|
|
44
|
+
colors = [];
|
|
45
|
+
query;
|
|
46
|
+
enabled = true;
|
|
47
|
+
constructor(language, timers = realTimers) {
|
|
48
|
+
this.language = language;
|
|
49
|
+
this.query = new DocumentQuery(language, timers, DELAY_MS, signal => this.enabled && this.supported ? language.service.documentColors(language.uri, {
|
|
50
|
+
signal
|
|
51
|
+
}) : Promise.resolve(null), colors => this.show(colors ?? []));
|
|
52
|
+
const {
|
|
53
|
+
editor
|
|
54
|
+
} = language;
|
|
55
|
+
if (editor.onInlayClick !== undefined) {
|
|
56
|
+
language.onDispose(editor.onInlayClick(click => this.pressed(click)));
|
|
57
|
+
}
|
|
58
|
+
language.onDispose(() => editor.setInlayHints([], OWNER));
|
|
59
|
+
}
|
|
60
|
+
get supported() {
|
|
61
|
+
const {
|
|
62
|
+
service
|
|
63
|
+
} = this.language;
|
|
64
|
+
return service.documentColors !== undefined && service.supports(METHOD, this.language.uri);
|
|
65
|
+
}
|
|
66
|
+
/* The colors of the current text, empty while an edit waits for the next answer. */
|
|
67
|
+
all() {
|
|
68
|
+
return this.colors;
|
|
69
|
+
}
|
|
70
|
+
/* Off, there are no swatches; on unless false. */
|
|
71
|
+
configure(settings) {
|
|
72
|
+
this.enabled = settings.enabled !== false;
|
|
73
|
+
if (this.enabled) {
|
|
74
|
+
this.query.run();
|
|
75
|
+
} else {
|
|
76
|
+
this.show([]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/* The ways the server writes a color found in the text, for a menu of the host. */
|
|
80
|
+
async presentations(information, color = information.color) {
|
|
81
|
+
const {
|
|
82
|
+
service
|
|
83
|
+
} = this.language;
|
|
84
|
+
if (service.colorPresentations === undefined) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return (await service.colorPresentations(this.language.uri, color, information.range)) ?? [];
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/* Writes a presentation in place of the color it was asked for; false when the text moved on since. */
|
|
94
|
+
apply(presentation, information) {
|
|
95
|
+
if (!this.colors.includes(information)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const changes = presentationEdits(presentation, information).map(edit => ({
|
|
99
|
+
range: edit.range,
|
|
100
|
+
text: edit.newText
|
|
101
|
+
}));
|
|
102
|
+
return this.language.editor.applyEdits(changes);
|
|
103
|
+
}
|
|
104
|
+
show(colors) {
|
|
105
|
+
this.colors = colors;
|
|
106
|
+
const hints = colors.map((information, index) => ({
|
|
107
|
+
id: `${ID_PREFIX}${index}`,
|
|
108
|
+
position: information.range.start,
|
|
109
|
+
label: this.language.project.i18n.t('editor:language.colors.swatch', {
|
|
110
|
+
color: hexOf(information.color)
|
|
111
|
+
}),
|
|
112
|
+
swatch: cssColorOf(information.color),
|
|
113
|
+
paddingRight: true
|
|
114
|
+
}));
|
|
115
|
+
this.language.editor.setInlayHints(hints, OWNER);
|
|
116
|
+
}
|
|
117
|
+
pressed(click) {
|
|
118
|
+
if (!click.id.startsWith(ID_PREFIX)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const information = this.colors[Number(click.id.slice(ID_PREFIX.length))];
|
|
122
|
+
if (information === undefined) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
void this.open(information);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
async open(information) {
|
|
129
|
+
const presentations = await this.presentations(information);
|
|
130
|
+
if (!this.colors.includes(information)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const {
|
|
134
|
+
pick,
|
|
135
|
+
project
|
|
136
|
+
} = this.language;
|
|
137
|
+
const rows = presentations.map((presentation, index) => ({
|
|
138
|
+
id: String(index),
|
|
139
|
+
label: presentation.label,
|
|
140
|
+
detail: ''
|
|
141
|
+
}));
|
|
142
|
+
pick.open({
|
|
143
|
+
anchor: information.range.start,
|
|
144
|
+
title: project.i18n.t('editor:language.colors.title'),
|
|
145
|
+
groups: [{
|
|
146
|
+
title: null,
|
|
147
|
+
rows
|
|
148
|
+
}, {
|
|
149
|
+
title: null,
|
|
150
|
+
rows: [{
|
|
151
|
+
id: OTHER_COLOR,
|
|
152
|
+
label: project.i18n.t('editor:language.colors.other'),
|
|
153
|
+
detail: ''
|
|
154
|
+
}]
|
|
155
|
+
}],
|
|
156
|
+
accept: id => {
|
|
157
|
+
if (id === OTHER_COLOR) {
|
|
158
|
+
this.choose(information, presentations);
|
|
159
|
+
} else {
|
|
160
|
+
this.apply(presentations[Number(id)], information);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/* The platform's color picker; the new color is written the way the old one was when the server offers that. */
|
|
166
|
+
choose(information, before) {
|
|
167
|
+
if (typeof document === 'undefined') {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const written = this.language.editor.textInRange(information.range);
|
|
171
|
+
const form = Math.max(0, before.findIndex(presentation => presentation.label === written));
|
|
172
|
+
const input = document.createElement('input');
|
|
173
|
+
input.type = 'color';
|
|
174
|
+
input.value = hexOf(information.color);
|
|
175
|
+
input.addEventListener('change', () => {
|
|
176
|
+
const color = colorOfHex(input.value);
|
|
177
|
+
if (color === null) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
void this.presentations(information, {
|
|
181
|
+
...color,
|
|
182
|
+
alpha: information.color.alpha
|
|
183
|
+
}).then(presentations => {
|
|
184
|
+
const presentation = presentations[form] ?? presentations[0];
|
|
185
|
+
if (presentation !== undefined) {
|
|
186
|
+
this.apply(presentation, information);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}, {
|
|
190
|
+
once: true
|
|
191
|
+
});
|
|
192
|
+
input.click();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
+
import type { DocumentLink } from '@adecore/lsp';
|
|
3
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
4
|
+
import { type Timers } from './timers.ts';
|
|
5
|
+
export declare function linkPosition(target: string): {
|
|
6
|
+
readonly uri: string;
|
|
7
|
+
readonly position: EditorPosition;
|
|
8
|
+
};
|
|
9
|
+
export declare class DocumentLinksFeature {
|
|
10
|
+
private readonly language;
|
|
11
|
+
private links;
|
|
12
|
+
private readonly query;
|
|
13
|
+
constructor(language: EditorLanguage, timers?: Timers);
|
|
14
|
+
get supported(): boolean;
|
|
15
|
+
all(): readonly DocumentLink[];
|
|
16
|
+
linkAt(position: EditorPosition): DocumentLink | null;
|
|
17
|
+
rangeAt(position: EditorPosition): EditorRange | null;
|
|
18
|
+
refresh(): void;
|
|
19
|
+
follow(link: DocumentLink): Promise<boolean>;
|
|
20
|
+
private resolve;
|
|
21
|
+
}
|
|
22
|
+
export declare function openTarget(language: EditorLanguage, target: string, selection?: EditorRange): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { comparePositions } from './diagnostics-model.js';
|
|
2
|
+
import { DocumentQuery } from './document-query.js';
|
|
3
|
+
import { realTimers } from './timers.js';
|
|
4
|
+
const METHOD = 'textDocument/documentLink';
|
|
5
|
+
const RESOLVE = 'documentLink/resolve';
|
|
6
|
+
/* How long after the last edit the links are asked again. */
|
|
7
|
+
const DELAY_MS = 500;
|
|
8
|
+
/* `#L12`, `#12` or `#L12,4`: a line, from one, and a column, from one, the form editors write into a link. */
|
|
9
|
+
const LINE_FRAGMENT = /^#L?(\d+)(?:[,:C](\d+))?$/i;
|
|
10
|
+
/* Where a link to a file of the editor lands, from the line its fragment names; the start of the file without one. */
|
|
11
|
+
export function linkPosition(target) {
|
|
12
|
+
const hash = target.indexOf('#');
|
|
13
|
+
const fragment = hash === -1 ? null : LINE_FRAGMENT.exec(target.slice(hash));
|
|
14
|
+
if (fragment === null) {
|
|
15
|
+
return {
|
|
16
|
+
uri: target,
|
|
17
|
+
position: {
|
|
18
|
+
line: 0,
|
|
19
|
+
character: 0
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
uri: target.slice(0, hash),
|
|
25
|
+
position: {
|
|
26
|
+
line: Math.max(0, Number(fragment[1]) - 1),
|
|
27
|
+
character: Math.max(0, Number(fragment[2] ?? 1) - 1)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/* Whether a target opens in the editor: a file, or a document of a scheme a server serves. A web address opens outside. */
|
|
32
|
+
function opensInEditor(target) {
|
|
33
|
+
return !/^(https?|mailto|ftp):/i.test(target);
|
|
34
|
+
}
|
|
35
|
+
/*
|
|
36
|
+
* The links a server finds in the text, such as an import path or a web address in a comment. Mod+click on one
|
|
37
|
+
* follows it, and while Mod is held the link under the pointer is drawn as one (`DefinitionLinkFeature` draws it).
|
|
38
|
+
* A link without a target asks the server for it first.
|
|
39
|
+
*/
|
|
40
|
+
export class DocumentLinksFeature {
|
|
41
|
+
language;
|
|
42
|
+
links = [];
|
|
43
|
+
query;
|
|
44
|
+
constructor(language, timers = realTimers) {
|
|
45
|
+
this.language = language;
|
|
46
|
+
this.query = new DocumentQuery(language, timers, DELAY_MS, signal => this.supported ? language.service.documentLinks(language.uri, {
|
|
47
|
+
signal
|
|
48
|
+
}) : Promise.resolve(null), links => {
|
|
49
|
+
this.links = links ?? [];
|
|
50
|
+
});
|
|
51
|
+
const off = language.editor.onClick(click => {
|
|
52
|
+
if (!click.mod || click.alt || click.shift) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const link = this.linkAt(click.position);
|
|
56
|
+
if (link === null) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
void this.follow(link);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
language.onDispose(off);
|
|
63
|
+
}
|
|
64
|
+
get supported() {
|
|
65
|
+
const {
|
|
66
|
+
service
|
|
67
|
+
} = this.language;
|
|
68
|
+
return service.documentLinks !== undefined && service.supports(METHOD, this.language.uri);
|
|
69
|
+
}
|
|
70
|
+
/* The links of the current text, empty while an edit waits for the next answer. */
|
|
71
|
+
all() {
|
|
72
|
+
return this.links;
|
|
73
|
+
}
|
|
74
|
+
/* The link whose range holds `position`, its end left out. */
|
|
75
|
+
linkAt(position) {
|
|
76
|
+
return this.links.find(link => comparePositions(link.range.start, position) <= 0 && comparePositions(position, link.range.end) < 0) ?? null;
|
|
77
|
+
}
|
|
78
|
+
/* The range of the link at `position`, which the pointer draws as a link while Mod is held. */
|
|
79
|
+
rangeAt(position) {
|
|
80
|
+
return this.linkAt(position)?.range ?? null;
|
|
81
|
+
}
|
|
82
|
+
refresh() {
|
|
83
|
+
this.query.run();
|
|
84
|
+
}
|
|
85
|
+
/* Opens what a link points at; false when it has no target, even after asking. */
|
|
86
|
+
async follow(link) {
|
|
87
|
+
const target = link.target ?? (await this.resolve(link))?.target;
|
|
88
|
+
if (target === undefined) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
openTarget(this.language, target);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
async resolve(link) {
|
|
95
|
+
const {
|
|
96
|
+
service
|
|
97
|
+
} = this.language;
|
|
98
|
+
if (service.resolveDocumentLink === undefined || service.providerOptions(RESOLVE, this.language.uri)?.resolveProvider !== true) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return await service.resolveDocumentLink(this.language.uri, link);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/* Opens a file or a document of a server in the editor, and a web address through the host. */
|
|
109
|
+
export function openTarget(language, target, selection) {
|
|
110
|
+
if (!opensInEditor(target)) {
|
|
111
|
+
const {
|
|
112
|
+
host
|
|
113
|
+
} = language.project;
|
|
114
|
+
if (host.openExternal !== undefined) {
|
|
115
|
+
host.openExternal(target);
|
|
116
|
+
} else if (typeof window !== 'undefined') {
|
|
117
|
+
window.open(target, '_blank', 'noopener,noreferrer');
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const {
|
|
122
|
+
uri,
|
|
123
|
+
position
|
|
124
|
+
} = linkPosition(target);
|
|
125
|
+
const range = selection ?? {
|
|
126
|
+
start: position,
|
|
127
|
+
end: position
|
|
128
|
+
};
|
|
129
|
+
language.goTo({
|
|
130
|
+
uri,
|
|
131
|
+
range
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
2
|
+
import type { Timers } from './timers.ts';
|
|
3
|
+
export declare class DocumentQuery<Value> {
|
|
4
|
+
private readonly language;
|
|
5
|
+
private readonly timers;
|
|
6
|
+
private readonly delay;
|
|
7
|
+
private readonly ask;
|
|
8
|
+
private readonly apply;
|
|
9
|
+
private timer;
|
|
10
|
+
private controller;
|
|
11
|
+
private generation;
|
|
12
|
+
constructor(language: EditorLanguage, timers: Timers, delay: number, ask: (signal: AbortSignal) => Promise<Value | null>, apply: (value: Value | null) => void);
|
|
13
|
+
run(): void;
|
|
14
|
+
private cancel;
|
|
15
|
+
}
|