@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.
Files changed (150) hide show
  1. package/dist/CallHierarchyPanel.js +177 -181
  2. package/dist/EditorContextMenu.js +118 -105
  3. package/dist/LanguagePopups.js +7 -0
  4. package/dist/PickPopup.js +245 -104
  5. package/dist/WrapCard.d.ts +8 -0
  6. package/dist/WrapCard.js +172 -0
  7. package/dist/call-hierarchy-model.d.ts +4 -2
  8. package/dist/call-hierarchy-model.js +20 -0
  9. package/dist/call-hierarchy.d.ts +3 -0
  10. package/dist/call-hierarchy.js +54 -22
  11. package/dist/clipboard-history.d.ts +31 -0
  12. package/dist/clipboard-history.js +70 -0
  13. package/dist/code-actions.d.ts +3 -1
  14. package/dist/code-actions.js +50 -10
  15. package/dist/code-vision.js +2 -3
  16. package/dist/completion.d.ts +28 -0
  17. package/dist/completion.js +83 -27
  18. package/dist/context-menu-rows.d.ts +1 -1
  19. package/dist/context-menu-rows.js +4 -0
  20. package/dist/definition-link.js +15 -5
  21. package/dist/document-colors.d.ts +24 -0
  22. package/dist/document-colors.js +194 -0
  23. package/dist/document-links.d.ts +22 -0
  24. package/dist/document-links.js +133 -0
  25. package/dist/document-query.d.ts +15 -0
  26. package/dist/document-query.js +54 -0
  27. package/dist/edit-locations.d.ts +20 -0
  28. package/dist/edit-locations.js +136 -0
  29. package/dist/editor-language.d.ts +23 -1
  30. package/dist/editor-language.js +83 -2
  31. package/dist/emmet.d.ts +13 -0
  32. package/dist/emmet.js +62 -0
  33. package/dist/folding.js +2 -3
  34. package/dist/format-on-type.js +3 -5
  35. package/dist/highlights.js +2 -3
  36. package/dist/history.d.ts +7 -0
  37. package/dist/history.js +79 -1
  38. package/dist/host-types.d.ts +10 -0
  39. package/dist/index.d.ts +13 -2
  40. package/dist/index.js +10 -0
  41. package/dist/inlay-hints.js +2 -2
  42. package/dist/inline-completion.d.ts +39 -0
  43. package/dist/inline-completion.js +221 -0
  44. package/dist/key-priority.d.ts +9 -5
  45. package/dist/key-priority.js +11 -6
  46. package/dist/linked-editing.d.ts +26 -0
  47. package/dist/linked-editing.js +215 -0
  48. package/dist/live-templates.d.ts +16 -0
  49. package/dist/live-templates.js +289 -0
  50. package/dist/locales/en.json +61 -2
  51. package/dist/locales/nl.json +61 -2
  52. package/dist/markup-context.d.ts +55 -0
  53. package/dist/markup-context.js +408 -0
  54. package/dist/markup.d.ts +39 -0
  55. package/dist/markup.js +465 -0
  56. package/dist/navigation.js +4 -6
  57. package/dist/paste-history.d.ts +7 -0
  58. package/dist/paste-history.js +104 -0
  59. package/dist/peek.js +3 -4
  60. package/dist/pick.d.ts +21 -2
  61. package/dist/pick.js +152 -19
  62. package/dist/popups.d.ts +10 -0
  63. package/dist/popups.js +2 -1
  64. package/dist/postfix.d.ts +21 -0
  65. package/dist/postfix.js +212 -0
  66. package/dist/project-language.d.ts +20 -1
  67. package/dist/project-language.js +132 -18
  68. package/dist/rename.js +8 -11
  69. package/dist/rendered-docs.js +1 -2
  70. package/dist/selection-ranges.js +2 -3
  71. package/dist/shortcut-keys.d.ts +24 -0
  72. package/dist/signature.js +3 -6
  73. package/dist/snippet-insert.d.ts +10 -0
  74. package/dist/snippet-insert.js +217 -0
  75. package/dist/snippet-session.d.ts +2 -2
  76. package/dist/snippet-session.js +3 -3
  77. package/dist/snippet.d.ts +7 -1
  78. package/dist/snippet.js +65 -8
  79. package/dist/surround-constructs.d.ts +10 -0
  80. package/dist/surround-constructs.js +100 -0
  81. package/dist/surround.d.ts +18 -0
  82. package/dist/surround.js +399 -0
  83. package/dist/symbol-picker.js +4 -6
  84. package/dist/symbols.js +22 -3
  85. package/dist/template-builtins.d.ts +3 -0
  86. package/dist/template-builtins.js +46 -0
  87. package/dist/template-context.d.ts +12 -0
  88. package/dist/template-context.js +56 -0
  89. package/dist/templates.d.ts +59 -0
  90. package/dist/templates.js +103 -0
  91. package/dist/testing.d.ts +23 -1
  92. package/dist/testing.js +114 -0
  93. package/package.json +7 -6
  94. package/src/CallHierarchyPanel.tsx +26 -11
  95. package/src/EditorContextMenu.tsx +7 -0
  96. package/src/LanguagePopups.tsx +4 -0
  97. package/src/PickPopup.tsx +47 -5
  98. package/src/WrapCard.tsx +64 -0
  99. package/src/call-hierarchy-model.ts +25 -3
  100. package/src/call-hierarchy.ts +62 -20
  101. package/src/clipboard-history.ts +102 -0
  102. package/src/code-actions.ts +48 -11
  103. package/src/code-vision.ts +3 -3
  104. package/src/completion.ts +123 -25
  105. package/src/context-menu-rows.ts +3 -2
  106. package/src/definition-link.ts +17 -5
  107. package/src/document-colors.ts +189 -0
  108. package/src/document-links.ts +129 -0
  109. package/src/document-query.ts +71 -0
  110. package/src/edit-locations.ts +162 -0
  111. package/src/editor-language.ts +86 -3
  112. package/src/emmet-module.d.ts +44 -0
  113. package/src/emmet.ts +73 -0
  114. package/src/folding.ts +3 -3
  115. package/src/format-on-type.ts +5 -5
  116. package/src/highlights.ts +3 -3
  117. package/src/history.ts +81 -1
  118. package/src/host-types.ts +17 -0
  119. package/src/index.ts +28 -3
  120. package/src/inlay-hints.ts +2 -2
  121. package/src/inline-completion.ts +221 -0
  122. package/src/key-priority.ts +11 -6
  123. package/src/linked-editing.ts +199 -0
  124. package/src/live-templates.ts +315 -0
  125. package/src/locales/en.json +61 -2
  126. package/src/locales/nl.json +61 -2
  127. package/src/markup-context.ts +387 -0
  128. package/src/markup.ts +377 -0
  129. package/src/navigation.ts +6 -6
  130. package/src/paste-history.ts +111 -0
  131. package/src/peek.ts +4 -4
  132. package/src/pick.ts +218 -20
  133. package/src/popups.ts +21 -1
  134. package/src/postfix.ts +224 -0
  135. package/src/project-language.ts +139 -19
  136. package/src/rename.ts +11 -11
  137. package/src/rendered-docs.ts +2 -2
  138. package/src/selection-ranges.ts +3 -3
  139. package/src/signature.ts +6 -6
  140. package/src/snippet-insert.ts +258 -0
  141. package/src/snippet-session.ts +9 -4
  142. package/src/snippet.ts +82 -7
  143. package/src/surround-constructs.ts +85 -0
  144. package/src/surround.ts +430 -0
  145. package/src/symbol-picker.ts +6 -6
  146. package/src/symbols.ts +30 -4
  147. package/src/template-builtins.ts +94 -0
  148. package/src/template-context.ts +65 -0
  149. package/src/templates.ts +197 -0
  150. package/src/testing.ts +126 -1
@@ -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) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
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
- project.service.executeCommand(uri, command).catch(() => undefined);
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 project.service.resolveCompletion(uri, item, {
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 project.service.completion(uri, editor.getCaret(), {
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 project.service.resolveCompletion(uri, fresh, {
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 project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
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 (!project.service.supports(METHOD, uri)) {
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
- try {
704
- const result = await project.service.completion(uri, position, context, {
705
- signal: controller.signal
706
- });
707
- if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
708
- return;
709
- }
710
- normalized = itemsOf(result);
711
- } catch {
712
- if (request === this.request && this.controller === controller && !controller.signal.aborted) {
713
- this.close();
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) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
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 project.service.resolveCompletion(uri, entry.item, {
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',
@@ -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. The editor only says where the pointer is, so the
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
- if (!this.held || pointer === null || !this.language.project.service.supports('textDocument/definition', this.language.uri)) {
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
- project.service.definition(uri, pointer, {
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
+ }