@adecore/editor-react 0.42.0 → 0.43.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 (122) 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 +190 -70
  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.js +10 -15
  17. package/dist/context-menu-rows.d.ts +1 -1
  18. package/dist/context-menu-rows.js +4 -0
  19. package/dist/definition-link.js +15 -5
  20. package/dist/document-colors.d.ts +24 -0
  21. package/dist/document-colors.js +194 -0
  22. package/dist/document-links.d.ts +22 -0
  23. package/dist/document-links.js +133 -0
  24. package/dist/document-query.d.ts +15 -0
  25. package/dist/document-query.js +54 -0
  26. package/dist/edit-locations.d.ts +20 -0
  27. package/dist/edit-locations.js +136 -0
  28. package/dist/editor-language.d.ts +19 -1
  29. package/dist/editor-language.js +77 -2
  30. package/dist/emmet.d.ts +13 -0
  31. package/dist/emmet.js +62 -0
  32. package/dist/folding.js +2 -3
  33. package/dist/format-on-type.js +3 -5
  34. package/dist/highlights.js +2 -3
  35. package/dist/history.d.ts +7 -0
  36. package/dist/history.js +79 -1
  37. package/dist/host-types.d.ts +6 -0
  38. package/dist/index.d.ts +11 -2
  39. package/dist/index.js +8 -0
  40. package/dist/inlay-hints.js +2 -2
  41. package/dist/inline-completion.d.ts +39 -0
  42. package/dist/inline-completion.js +221 -0
  43. package/dist/key-priority.d.ts +8 -5
  44. package/dist/key-priority.js +9 -6
  45. package/dist/linked-editing.d.ts +26 -0
  46. package/dist/linked-editing.js +215 -0
  47. package/dist/locales/en.json +31 -2
  48. package/dist/locales/nl.json +31 -2
  49. package/dist/markup-context.d.ts +37 -0
  50. package/dist/markup-context.js +291 -0
  51. package/dist/markup.d.ts +36 -0
  52. package/dist/markup.js +453 -0
  53. package/dist/navigation.js +4 -6
  54. package/dist/paste-history.d.ts +7 -0
  55. package/dist/paste-history.js +104 -0
  56. package/dist/peek.js +3 -4
  57. package/dist/pick.d.ts +20 -2
  58. package/dist/pick.js +141 -18
  59. package/dist/popups.d.ts +9 -0
  60. package/dist/popups.js +2 -1
  61. package/dist/project-language.d.ts +18 -1
  62. package/dist/project-language.js +129 -18
  63. package/dist/rename.js +8 -11
  64. package/dist/rendered-docs.js +1 -2
  65. package/dist/selection-ranges.js +2 -3
  66. package/dist/shortcut-keys.d.ts +19 -0
  67. package/dist/signature.js +3 -6
  68. package/dist/snippet-session.d.ts +2 -2
  69. package/dist/snippet-session.js +3 -3
  70. package/dist/symbol-picker.js +4 -6
  71. package/dist/symbols.js +22 -3
  72. package/dist/testing.d.ts +23 -1
  73. package/dist/testing.js +114 -0
  74. package/package.json +7 -6
  75. package/src/CallHierarchyPanel.tsx +26 -11
  76. package/src/EditorContextMenu.tsx +7 -0
  77. package/src/LanguagePopups.tsx +4 -0
  78. package/src/PickPopup.tsx +40 -5
  79. package/src/WrapCard.tsx +64 -0
  80. package/src/call-hierarchy-model.ts +25 -3
  81. package/src/call-hierarchy.ts +62 -20
  82. package/src/clipboard-history.ts +102 -0
  83. package/src/code-actions.ts +48 -11
  84. package/src/code-vision.ts +3 -3
  85. package/src/completion.ts +15 -15
  86. package/src/context-menu-rows.ts +3 -2
  87. package/src/definition-link.ts +17 -5
  88. package/src/document-colors.ts +189 -0
  89. package/src/document-links.ts +129 -0
  90. package/src/document-query.ts +71 -0
  91. package/src/edit-locations.ts +162 -0
  92. package/src/editor-language.ts +80 -3
  93. package/src/emmet-module.d.ts +44 -0
  94. package/src/emmet.ts +73 -0
  95. package/src/folding.ts +3 -3
  96. package/src/format-on-type.ts +5 -5
  97. package/src/highlights.ts +3 -3
  98. package/src/history.ts +81 -1
  99. package/src/host-types.ts +10 -0
  100. package/src/index.ts +12 -3
  101. package/src/inlay-hints.ts +2 -2
  102. package/src/inline-completion.ts +221 -0
  103. package/src/key-priority.ts +9 -6
  104. package/src/linked-editing.ts +199 -0
  105. package/src/locales/en.json +31 -2
  106. package/src/locales/nl.json +31 -2
  107. package/src/markup-context.ts +262 -0
  108. package/src/markup.ts +367 -0
  109. package/src/navigation.ts +6 -6
  110. package/src/paste-history.ts +111 -0
  111. package/src/peek.ts +4 -4
  112. package/src/pick.ts +201 -19
  113. package/src/popups.ts +19 -1
  114. package/src/project-language.ts +136 -19
  115. package/src/rename.ts +11 -11
  116. package/src/rendered-docs.ts +2 -2
  117. package/src/selection-ranges.ts +3 -3
  118. package/src/signature.ts +6 -6
  119. package/src/snippet-session.ts +9 -4
  120. package/src/symbol-picker.ts +6 -6
  121. package/src/symbols.ts +30 -4
  122. package/src/testing.ts +126 -1
@@ -0,0 +1,64 @@
1
+ import { useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { EditorRect } from '@adecore/editor';
4
+ import { Input } from '@adecore/ui';
5
+ import { AnchoredPopup } from './AnchoredPopup.tsx';
6
+ import type { EditorLanguage } from './editor-language.ts';
7
+ import type { WrapView } from './popups.ts';
8
+
9
+ /* The field for the abbreviation a selection is wrapped in: Enter wraps, Escape or leaving the field cancels. */
10
+ export function WrapCard({ language, view, rect }: { language: EditorLanguage; view: WrapView; rect: EditorRect }) {
11
+ const { t } = useTranslation('editor');
12
+ const input = useRef<HTMLInputElement>(null);
13
+ const [value, setValue] = useState('');
14
+
15
+ useLayoutEffect(() => {
16
+ input.current?.focus();
17
+ }, []);
18
+
19
+ function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
20
+ if (event.nativeEvent.isComposing) {
21
+ return;
22
+ }
23
+ if (event.key === 'Escape') {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ language.markup.cancelWrap();
27
+ language.editor.focus();
28
+ } else if (event.key === 'Enter') {
29
+ event.preventDefault();
30
+ event.stopPropagation();
31
+ if (value.trim() !== '') {
32
+ language.markup.submitWrap(value);
33
+ }
34
+ }
35
+ }
36
+
37
+ return (
38
+ <AnchoredPopup rect={rect} placement={{ prefer: 'above' }}>
39
+ <div className="flex w-[280px] flex-col gap-1.5 p-2">
40
+ <Input
41
+ ref={input}
42
+ size="sm"
43
+ mono
44
+ value={value}
45
+ spellCheck={false}
46
+ autoComplete="off"
47
+ aria-label={t('language.markup.wrap')}
48
+ aria-invalid={view.error !== null}
49
+ placeholder={t('language.markup.placeholder')}
50
+ onChange={(event) => setValue(event.target.value)}
51
+ onKeyDown={onKeyDown}
52
+ onBlur={() => language.markup.cancelWrap()}
53
+ />
54
+ {view.error !== null ? (
55
+ <div role="alert" className="text-xs text-status-error">
56
+ {view.error}
57
+ </div>
58
+ ) : (
59
+ <div className="text-xs text-text-muted">↵ {t('language.markup.apply')}</div>
60
+ )}
61
+ </div>
62
+ </AnchoredPopup>
63
+ );
64
+ }
@@ -1,7 +1,12 @@
1
- import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range } from '@adecore/lsp';
1
+ import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range, TypeHierarchyItem } from '@adecore/lsp';
2
2
 
3
- /* Callers of a symbol, or the symbols it calls. */
4
- export type CallDirection = 'incoming' | 'outgoing';
3
+ /* Callers of a symbol or the symbols it calls; or the super and sub types of a type, which the same tree draws. */
4
+ export type CallDirection = 'incoming' | 'outgoing' | 'supertypes' | 'subtypes';
5
+
6
+ /* Whether a direction walks a type hierarchy and not the calls. */
7
+ export function isTypeDirection(direction: CallDirection): direction is 'supertypes' | 'subtypes' {
8
+ return direction === 'supertypes' || direction === 'subtypes';
9
+ }
5
10
 
6
11
  /* The most calls of one level that are drawn at once; the rest wait behind a row that shows more. */
7
12
  export const CALL_LEVEL_LIMIT = 50;
@@ -103,6 +108,23 @@ export function childrenOf(direction: CallDirection, parent: CallNode, calls: re
103
108
  }));
104
109
  }
105
110
 
111
+ /* The children of a node made from the types a server returned; a type has no call sites, so a row goes to the type itself. */
112
+ export function typeChildrenOf(parent: CallNode, items: readonly TypeHierarchyItem[]): CallNode[] {
113
+ return items.map((item, index) => ({
114
+ id: `${parent.id}.${index}`,
115
+ parent: parent.id,
116
+ level: parent.level + 1,
117
+ item,
118
+ sites: [],
119
+ siteUri: null,
120
+ expanded: false,
121
+ load: 'idle',
122
+ error: null,
123
+ children: [],
124
+ shown: 0
125
+ }));
126
+ }
127
+
106
128
  /* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
107
129
  export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly string[]): CallRow[] {
108
130
  const rows: CallRow[] = [];
@@ -5,12 +5,14 @@ import type { EditorLanguage } from './editor-language.ts';
5
5
  import {
6
6
  CALL_LEVEL_LIMIT,
7
7
  childrenOf,
8
+ isTypeDirection,
8
9
  moreIdOf,
9
10
  rootsOf,
10
11
  rowsOf,
11
12
  statusIdOf,
12
13
  stepsOf,
13
14
  targetOf,
15
+ typeChildrenOf,
14
16
  type CallDirection,
15
17
  type CallNode,
16
18
  type CallRow
@@ -23,6 +25,7 @@ import { realTimers, revealRow, type Timers } from './timers.ts';
23
25
 
24
26
  const TOAST_ID = 'language-call-hierarchy';
25
27
  const PREPARE = 'textDocument/prepareCallHierarchy';
28
+ const PREPARE_TYPES = 'textDocument/prepareTypeHierarchy';
26
29
 
27
30
  /* The row the panel stands in is as tall as a peek's, plus the segmented control that is a pixel taller than the peek's title bar. */
28
31
  export const CALL_HIERARCHY_HEIGHT = PEEK_HEIGHT + 2;
@@ -42,6 +45,7 @@ export class CallHierarchyFeature {
42
45
  private nodes = new Map<string, CallNode>();
43
46
  private roots: string[] = [];
44
47
  private prepared: CallHierarchyItem[] = [];
48
+ private position: EditorPosition | null = null;
45
49
  private direction: CallDirection = 'incoming';
46
50
  private controller = new AbortController();
47
51
 
@@ -58,11 +62,13 @@ export class CallHierarchyFeature {
58
62
 
59
63
  const offs = [
60
64
  language.onKey((event) => {
61
- if (isShortcut(this.language.shortcuts.callHierarchy, event)) {
62
- if (this.isOpen) {
63
- this.setDirection(this.direction === 'incoming' ? 'outgoing' : 'incoming');
65
+ const calls = isShortcut(this.language.shortcuts.callHierarchy, event);
66
+ if (calls || isShortcut(this.language.shortcuts.typeHierarchy, event)) {
67
+ const [first, second]: CallDirection[] = calls ? ['incoming', 'outgoing'] : ['supertypes', 'subtypes'];
68
+ if (this.isOpen && isTypeDirection(this.direction) !== calls) {
69
+ this.setDirection(this.direction === first ? second : first);
64
70
  } else {
65
- void this.open();
71
+ void this.open(undefined, first);
66
72
  }
67
73
 
68
74
  return true;
@@ -88,8 +94,7 @@ export class CallHierarchyFeature {
88
94
 
89
95
  /* Whether a server of this document answers the three requests the tree is made of. */
90
96
  get supported(): boolean {
91
- const { project, uri } = this.language;
92
- const { service } = project;
97
+ const { service, uri } = this.language;
93
98
 
94
99
  return (
95
100
  service.prepareCallHierarchy !== undefined &&
@@ -99,12 +104,28 @@ export class CallHierarchyFeature {
99
104
  );
100
105
  }
101
106
 
102
- /* Opens the tree for the symbol at the position, on its callers unless told otherwise. */
107
+ /* Whether a server of this document answers the three requests of the type hierarchy. */
108
+ get typesSupported(): boolean {
109
+ const { service, uri } = this.language;
110
+
111
+ return (
112
+ service.prepareTypeHierarchy !== undefined &&
113
+ service.supertypes !== undefined &&
114
+ service.subtypes !== undefined &&
115
+ service.supports(PREPARE_TYPES, uri)
116
+ );
117
+ }
118
+
119
+ /*
120
+ * Opens the tree for the symbol at the position, on its callers unless told otherwise; `supertypes` or `subtypes`
121
+ * open the type hierarchy of the type at the position.
122
+ */
103
123
  async open(position: EditorPosition = this.language.editor.getCaret(), direction: CallDirection = 'incoming'): Promise<void> {
104
- const { editor, project, uri } = this.language;
124
+ const { editor, service, uri } = this.language;
125
+ const types = isTypeDirection(direction);
105
126
 
106
- if (!this.supported) {
107
- this.tell(this.say('unavailable'));
127
+ if (!(types ? this.typesSupported : this.supported)) {
128
+ this.tell(this.say(types ? 'typesUnavailable' : 'unavailable'));
108
129
 
109
130
  return;
110
131
  }
@@ -114,10 +135,12 @@ export class CallHierarchyFeature {
114
135
  let items: CallHierarchyItem[] | null;
115
136
 
116
137
  try {
117
- items = await project.service.prepareCallHierarchy!(uri, position, { signal: this.controller.signal });
138
+ const options = { signal: this.controller.signal };
139
+
140
+ items = types ? await service.prepareTypeHierarchy!(uri, position, options) : await service.prepareCallHierarchy!(uri, position, options);
118
141
  } catch (error) {
119
142
  if (epoch === this.epoch && !(error instanceof StaleResultError)) {
120
- this.tell(this.say('failed', { message: messageOf(error) }));
143
+ this.tell(this.say(types ? 'typesFailed' : 'failed', { message: messageOf(error) }));
121
144
  }
122
145
 
123
146
  return;
@@ -128,13 +151,14 @@ export class CallHierarchyFeature {
128
151
  }
129
152
 
130
153
  if (items === null || items.length === 0) {
131
- this.tell(this.say('none'));
154
+ this.tell(this.say(types ? 'typesNone' : 'none'));
132
155
 
133
156
  return;
134
157
  }
135
158
 
136
159
  this.language.peek.close();
137
160
  this.prepared = items;
161
+ this.position = position;
138
162
  this.direction = direction;
139
163
  this.seed();
140
164
  const active = this.roots[0]!;
@@ -171,7 +195,7 @@ export class CallHierarchyFeature {
171
195
  this.language.editor.focus();
172
196
  }
173
197
 
174
- /* Starts the tree over from the same symbols for the other direction. */
198
+ /* Starts the tree over from the same symbols for the other direction; a switch between calls and types prepares the symbol again. */
175
199
  setDirection(direction: CallDirection): void {
176
200
  const view = this.language.popups.getState().callHierarchy;
177
201
 
@@ -179,6 +203,12 @@ export class CallHierarchyFeature {
179
203
  return;
180
204
  }
181
205
 
206
+ if (isTypeDirection(direction) !== isTypeDirection(view.direction)) {
207
+ void this.open(this.position ?? undefined, direction);
208
+
209
+ return;
210
+ }
211
+
182
212
  this.reset();
183
213
  this.direction = direction;
184
214
  this.seed();
@@ -331,22 +361,19 @@ export class CallHierarchyFeature {
331
361
  return;
332
362
  }
333
363
 
334
- const { service, i18n } = this.language.project;
335
- const { uri, editor } = this.language;
364
+ const { i18n } = this.language.project;
365
+ const { editor } = this.language;
336
366
  const { direction, epoch } = this;
337
367
  const options = { signal: this.controller.signal, parallel: true };
338
368
  this.patch(id, { load: 'loading', error: null });
339
369
 
340
370
  try {
341
- const calls =
342
- direction === 'incoming' ? await service.incomingCalls!(uri, node.item, options) : await service.outgoingCalls!(uri, node.item, options);
371
+ const children = await this.childrenOf(direction, node, options);
343
372
 
344
373
  if (epoch !== this.epoch) {
345
374
  return;
346
375
  }
347
376
 
348
- const children = childrenOf(direction, node, calls ?? []);
349
-
350
377
  for (const child of children) {
351
378
  this.nodes.set(child.id, child);
352
379
  }
@@ -368,6 +395,21 @@ export class CallHierarchyFeature {
368
395
  }
369
396
  }
370
397
 
398
+ private async childrenOf(direction: CallDirection, node: CallNode, options: { signal: AbortSignal; parallel: boolean }): Promise<CallNode[]> {
399
+ const { service, uri } = this.language;
400
+
401
+ switch (direction) {
402
+ case 'incoming':
403
+ return childrenOf(direction, node, (await service.incomingCalls!(uri, node.item, options)) ?? []);
404
+ case 'outgoing':
405
+ return childrenOf(direction, node, (await service.outgoingCalls!(uri, node.item, options)) ?? []);
406
+ case 'supertypes':
407
+ return typeChildrenOf(node, (await service.supertypes!(uri, node.item, options)) ?? []);
408
+ case 'subtypes':
409
+ return typeChildrenOf(node, (await service.subtypes!(uri, node.item, options)) ?? []);
410
+ }
411
+ }
412
+
371
413
  private async preview(node: CallNode, site: number): Promise<void> {
372
414
  const { project } = this.language;
373
415
  const epoch = ++this.previewEpoch;
@@ -0,0 +1,102 @@
1
+ /* One copy or cut, as the editors of a project made it. */
2
+ export interface ClipboardEntry {
3
+ readonly text: string;
4
+ /* The copy took the lines of bare carets, which paste back above the lines of bare carets. */
5
+ readonly wholeLines: boolean;
6
+ readonly at: number;
7
+ }
8
+
9
+ export interface ClipboardHistoryOptions {
10
+ /* How many entries the history keeps; 100. */
11
+ readonly limit?: number;
12
+ /* The UTF-16 length all entries may take together; 10,000,000. The oldest go first, and the newest stays even when it alone is longer. */
13
+ readonly maxLength?: number;
14
+ /* What a host kept, newest first. */
15
+ readonly entries?: readonly ClipboardEntry[];
16
+ /* Told after every change, for a host that keeps the entries. */
17
+ onChange?(entries: readonly ClipboardEntry[]): void;
18
+ readonly clock?: () => number;
19
+ }
20
+
21
+ /* The texts copied or cut in the editors of a project, newest first, for Paste from History. */
22
+ export class ClipboardHistory {
23
+ private readonly limit: number;
24
+ private readonly maxLength: number;
25
+ private readonly onChange: ((entries: readonly ClipboardEntry[]) => void) | undefined;
26
+ private readonly clock: () => number;
27
+ private readonly listeners = new Set<() => void>();
28
+ private list: readonly ClipboardEntry[];
29
+
30
+ constructor(options: ClipboardHistoryOptions = {}) {
31
+ this.limit = options.limit ?? 100;
32
+ this.maxLength = options.maxLength ?? 10_000_000;
33
+ this.onChange = options.onChange;
34
+ this.clock = options.clock ?? Date.now;
35
+ this.list = this.trimmed(options.entries ?? []);
36
+ }
37
+
38
+ /* Newest first. */
39
+ get entries(): readonly ClipboardEntry[] {
40
+ return this.list;
41
+ }
42
+
43
+ /* Puts a copy on top. The same text again moves up instead of being kept twice, and nothing is kept for an empty text. */
44
+ add(text: string, options: { readonly wholeLines?: boolean } = {}): void {
45
+ if (text === '') {
46
+ return;
47
+ }
48
+
49
+ const entry = { text, wholeLines: options.wholeLines === true, at: this.clock() };
50
+
51
+ this.change([entry, ...this.list.filter((other) => other.text !== text)]);
52
+ }
53
+
54
+ remove(entry: ClipboardEntry): void {
55
+ if (this.list.includes(entry)) {
56
+ this.change(this.list.filter((other) => other !== entry));
57
+ }
58
+ }
59
+
60
+ /* Moves an entry to the top, as choosing it does. */
61
+ promote(entry: ClipboardEntry): void {
62
+ if (this.list.includes(entry) && this.list[0] !== entry) {
63
+ this.change([entry, ...this.list.filter((other) => other !== entry)]);
64
+ }
65
+ }
66
+
67
+ clear(): void {
68
+ if (this.list.length > 0) {
69
+ this.change([]);
70
+ }
71
+ }
72
+
73
+ subscribe(listener: () => void): () => void {
74
+ this.listeners.add(listener);
75
+
76
+ return () => this.listeners.delete(listener);
77
+ }
78
+
79
+ private change(entries: readonly ClipboardEntry[]): void {
80
+ this.list = this.trimmed(entries);
81
+ this.onChange?.(this.list);
82
+ this.listeners.forEach((listener) => listener());
83
+ }
84
+
85
+ /* Keeps the newest entries that fit the limit and the total length. */
86
+ private trimmed(entries: readonly ClipboardEntry[]): readonly ClipboardEntry[] {
87
+ const kept: ClipboardEntry[] = [];
88
+ let length = 0;
89
+
90
+ for (const entry of entries.slice(0, this.limit)) {
91
+ length += entry.text.length;
92
+
93
+ if (kept.length > 0 && length > this.maxLength) {
94
+ break;
95
+ }
96
+
97
+ kept.push(entry);
98
+ }
99
+
100
+ return kept;
101
+ }
102
+ }
@@ -13,7 +13,7 @@ import {
13
13
  type ActionEntry,
14
14
  type ActionGroup
15
15
  } from './code-actions-model.ts';
16
- import type { Problem } from './diagnostics-model.ts';
16
+ import { comparePositions, type Problem } from './diagnostics-model.ts';
17
17
  import type { EditorLanguage } from './editor-language.ts';
18
18
  import type { PickGroup, PickPreview } from './popups.ts';
19
19
  import { Refresher } from './refresher.ts';
@@ -122,7 +122,7 @@ export class CodeActionsFeature {
122
122
  }
123
123
 
124
124
  get supported(): boolean {
125
- return this.language.project.service.supports(METHOD, this.language.uri);
125
+ return this.language.service.supports(METHOD, this.language.uri);
126
126
  }
127
127
 
128
128
  /* The list under the caret, or under the selection's start. */
@@ -162,16 +162,16 @@ export class CodeActionsFeature {
162
162
  }
163
163
 
164
164
  async formatDocument(): Promise<void> {
165
- const { editor, project, uri } = this.language;
165
+ const { editor, uri } = this.language;
166
166
 
167
- if (!project.service.supports('textDocument/formatting', uri)) {
167
+ if (!this.language.service.supports('textDocument/formatting', uri)) {
168
168
  this.tell('error', this.say('noFormatter'));
169
169
 
170
170
  return;
171
171
  }
172
172
 
173
173
  try {
174
- const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
174
+ const edits = await this.language.service.formatting(uri, { ...editor.getIndentation() });
175
175
 
176
176
  if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
177
177
  this.tell('error', this.say('readOnly'));
@@ -181,6 +181,43 @@ export class CodeActionsFeature {
181
181
  }
182
182
  }
183
183
 
184
+ /* Whether a server formats a part of this document, so `formatSelection` has something to ask. */
185
+ get formatsSelection(): boolean {
186
+ return this.language.service.supports('textDocument/rangeFormatting', this.language.uri);
187
+ }
188
+
189
+ /*
190
+ * Formats what is selected, every selection in one request when the server formats several ranges at once and
191
+ * one by one otherwise; the whole document when nothing is selected.
192
+ */
193
+ async formatSelection(): Promise<void> {
194
+ const { editor, uri } = this.language;
195
+ const ranges = editor.getSelections().filter((range) => comparePositions(range.start, range.end) !== 0);
196
+ if (ranges.length === 0) {
197
+ await this.formatDocument();
198
+ return;
199
+ }
200
+ if (!this.formatsSelection) {
201
+ this.tell('error', this.say('noFormatter'));
202
+ return;
203
+ }
204
+ const { service } = this.language;
205
+ const formatting = { ...editor.getIndentation() };
206
+ try {
207
+ const edits =
208
+ service.rangesFormatting !== undefined && service.supports('textDocument/rangesFormatting', uri)
209
+ ? await service.rangesFormatting(uri, ranges, formatting)
210
+ : (await Promise.all(ranges.map((range) => service.rangeFormatting(uri, range, formatting, { parallel: true })))).flatMap(
211
+ (list) => list ?? []
212
+ );
213
+ if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
214
+ this.tell('error', this.say('readOnly'));
215
+ }
216
+ } catch (error) {
217
+ this.fail(error);
218
+ }
219
+ }
220
+
184
221
  /* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
185
222
  list(only: readonly string[]): Promise<ActionEntry[] | null> {
186
223
  if (!this.supported) {
@@ -247,7 +284,7 @@ export class CodeActionsFeature {
247
284
 
248
285
  /* Asks the servers; null when they could not answer, with the reason said aloud for a request a person made. */
249
286
  private async request(range: EditorRange, triggerKind: number, options: RequestOptions = {}): Promise<ActionEntry[] | null> {
250
- const { project, uri, diagnostics } = this.language;
287
+ const { uri, diagnostics } = this.language;
251
288
  const { only, signal, order } = options;
252
289
 
253
290
  try {
@@ -264,7 +301,7 @@ export class CodeActionsFeature {
264
301
  triggerKind: triggerKind as 1 | 2
265
302
  };
266
303
 
267
- const entries = actionsOf(await project.service.codeActions(uri, range, context, { signal }), order);
304
+ const entries = actionsOf(await this.language.service.codeActions(uri, range, context, { signal }), order);
268
305
 
269
306
  // A server may answer with more than the kinds asked for.
270
307
  return only ? entries.filter((entry) => only.some((kind) => entry.action.kind === kind || entry.action.kind?.startsWith(`${kind}.`))) : entries;
@@ -329,13 +366,13 @@ export class CodeActionsFeature {
329
366
  return known;
330
367
  }
331
368
 
332
- const { project, uri } = this.language;
369
+ const { uri } = this.language;
333
370
  const { action } = entry;
334
371
  let result = action;
335
372
 
336
- if (action.edit === undefined && project.service.providerOptions(METHOD, uri)?.resolveProvider === true) {
373
+ if (action.edit === undefined && this.language.service.providerOptions(METHOD, uri)?.resolveProvider === true) {
337
374
  try {
338
- result = await project.service.resolveCodeAction(uri, action, { signal });
375
+ result = await this.language.service.resolveCodeAction(uri, action, { signal });
339
376
  } catch (error) {
340
377
  if (isRefusal(error)) {
341
378
  throw new ActionRefused((error as Error).message);
@@ -427,7 +464,7 @@ export class CodeActionsFeature {
427
464
  if (action.command?.command === INLINE_RENAME_COMMAND) {
428
465
  await this.startRename(action.command);
429
466
  } else if (action.command !== undefined) {
430
- await project.service.executeCommand(uri, action.command);
467
+ await this.language.service.executeCommand(uri, action.command);
431
468
  }
432
469
  } catch (error) {
433
470
  this.fail(error);
@@ -149,7 +149,7 @@ export class CodeVisionFeature {
149
149
  }
150
150
 
151
151
  private get canCount(): boolean {
152
- return this.settings.usages && this.language.project.service.supports(REFERENCES, this.language.uri);
152
+ return this.settings.usages && this.language.service.supports(REFERENCES, this.language.uri);
153
153
  }
154
154
 
155
155
  /* Queues the declarations in view whose count is not from the current text, nearest the top of the view first. */
@@ -180,13 +180,13 @@ export class CodeVisionFeature {
180
180
  }
181
181
 
182
182
  private async count(declaration: CodeVisionDeclaration): Promise<void> {
183
- const { project, uri } = this.language;
183
+ const { uri } = this.language;
184
184
  const controller = new AbortController();
185
185
  this.running.set(declaration.id, controller);
186
186
  const epoch = this.epoch;
187
187
 
188
188
  try {
189
- const locations = await project.service.references(uri, declaration.position, false, { signal: controller.signal, parallel: true });
189
+ const locations = await this.language.service.references(uri, declaration.position, false, { signal: controller.signal, parallel: true });
190
190
 
191
191
  if (!this.disposed && epoch === this.epoch) {
192
192
  this.counts.set(declaration.id, { count: distinctPlaces(locations ?? []), epoch });
package/src/completion.ts CHANGED
@@ -236,7 +236,7 @@ export class CompletionFeature {
236
236
 
237
237
  recentChoicesOf(this.language.languageId).record(entry.item);
238
238
  let item = this.resolved.get(entry.item) ?? entry.item;
239
- const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
239
+ const needsResolve = !this.resolved.has(entry.item) && this.language.service.providerOptions('completionItem/resolve', uri) !== undefined;
240
240
  this.close();
241
241
 
242
242
  if (needsResolve) {
@@ -522,7 +522,7 @@ export class CompletionFeature {
522
522
 
523
523
  /* Parameter info for a call that was opened (or that the server asks for), then the item's own command. */
524
524
  private afterInsert(item: CompletionItem, commit: string | undefined, lineBefore: string): void {
525
- const { editor, project, uri, signature } = this.language;
525
+ const { editor, uri, signature } = this.language;
526
526
  const { start } = editor.getSelection();
527
527
  const insideCall = start.character > 0 && editor.textInRange({ start: { line: start.line, character: start.character - 1 }, end: start }) === '(';
528
528
 
@@ -539,7 +539,7 @@ export class CompletionFeature {
539
539
  if (command.command === PARAMETER_HINTS_COMMAND) {
540
540
  signature.trigger();
541
541
  } else {
542
- project.service.executeCommand(uri, command).catch(() => undefined);
542
+ this.language.service.executeCommand(uri, command).catch(() => undefined);
543
543
  }
544
544
  }
545
545
 
@@ -549,10 +549,10 @@ export class CompletionFeature {
549
549
  * suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
550
550
  */
551
551
  private async resolveNow(item: CompletionItem, signal: AbortSignal): Promise<CompletionItem> {
552
- const { editor, project, uri } = this.language;
552
+ const { editor, uri } = this.language;
553
553
 
554
554
  try {
555
- return { ...item, ...(await project.service.resolveCompletion(uri, item, { signal })) };
555
+ return { ...item, ...(await this.language.service.resolveCompletion(uri, item, { signal })) };
556
556
  } catch (error) {
557
557
  if (!(error instanceof StaleResultError)) {
558
558
  return item;
@@ -560,11 +560,11 @@ export class CompletionFeature {
560
560
  }
561
561
 
562
562
  try {
563
- const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find(
563
+ const fresh = itemsOf(await this.language.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find(
564
564
  (candidate) => candidate.label === item.label && candidate.kind === item.kind
565
565
  );
566
566
 
567
- return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
567
+ return fresh === undefined ? item : { ...fresh, ...(await this.language.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
568
568
  } catch {
569
569
  return item;
570
570
  }
@@ -703,15 +703,15 @@ export class CompletionFeature {
703
703
  }
704
704
 
705
705
  private triggerCharacters(): readonly string[] {
706
- const { project, uri } = this.language;
706
+ const { uri } = this.language;
707
707
 
708
- return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
708
+ return this.language.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
709
709
  }
710
710
 
711
711
  private beginRequest(context: CompletionContext, delay: number): void {
712
- const { project, uri } = this.language;
712
+ const { uri } = this.language;
713
713
 
714
- if (!project.service.supports(METHOD, uri)) {
714
+ if (!this.language.service.supports(METHOD, uri)) {
715
715
  return;
716
716
  }
717
717
 
@@ -729,7 +729,7 @@ export class CompletionFeature {
729
729
  let normalized: ReturnType<typeof itemsOf>;
730
730
 
731
731
  try {
732
- const result = await project.service.completion(uri, position, context, { signal: controller.signal });
732
+ const result = await this.language.service.completion(uri, position, context, { signal: controller.signal });
733
733
 
734
734
  if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
735
735
  return;
@@ -839,12 +839,12 @@ export class CompletionFeature {
839
839
  }
840
840
 
841
841
  private scheduleResolve(): void {
842
- const { project, uri } = this.language;
842
+ const { uri } = this.language;
843
843
  this.timers.clear(this.resolveTimer);
844
844
  this.resolving?.abort();
845
845
  const entry = this.ranked[this.active];
846
846
 
847
- if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
847
+ if (entry === undefined || this.resolved.has(entry.item) || this.language.service.providerOptions('completionItem/resolve', uri) === undefined) {
848
848
  return;
849
849
  }
850
850
 
@@ -854,7 +854,7 @@ export class CompletionFeature {
854
854
  const controller = new AbortController();
855
855
  this.resolving = controller;
856
856
 
857
- void project.service
857
+ void this.language.service
858
858
  .resolveCompletion(uri, entry.item, { signal: controller.signal })
859
859
  .then((item) => {
860
860
  this.resolved.set(entry.item, { ...entry.item, ...item });
@@ -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[] = [