@adecore/editor-react 0.21.0 → 0.21.2

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 (109) hide show
  1. package/dist/AnchoredPopup.js +2 -3
  2. package/dist/HoverSections.js +5 -11
  3. package/dist/PeekPanel.js +6 -13
  4. package/dist/RenameCard.js +2 -2
  5. package/dist/SymbolPicker.js +2 -3
  6. package/dist/code-actions-model.js +9 -8
  7. package/dist/code-actions.d.ts +1 -1
  8. package/dist/code-actions.js +21 -20
  9. package/dist/code-authors.js +1 -1
  10. package/dist/code-vision.d.ts +1 -0
  11. package/dist/code-vision.js +17 -14
  12. package/dist/completion-call.js +2 -6
  13. package/dist/completion-model.d.ts +3 -2
  14. package/dist/completion-model.js +8 -9
  15. package/dist/completion.js +8 -10
  16. package/dist/context-menu.js +4 -5
  17. package/dist/definition-link.js +2 -1
  18. package/dist/diagnostics-model.d.ts +2 -0
  19. package/dist/diagnostics-model.js +8 -2
  20. package/dist/diagnostics.d.ts +1 -0
  21. package/dist/diagnostics.js +13 -12
  22. package/dist/editor-language.d.ts +2 -2
  23. package/dist/editor-language.js +10 -10
  24. package/dist/error-message.d.ts +1 -0
  25. package/dist/error-message.js +3 -0
  26. package/dist/folding.js +5 -24
  27. package/dist/history.d.ts +1 -0
  28. package/dist/history.js +9 -12
  29. package/dist/hover-content.d.ts +1 -0
  30. package/dist/hover-content.js +5 -4
  31. package/dist/hover.d.ts +2 -2
  32. package/dist/hover.js +10 -9
  33. package/dist/inlay-hints.js +4 -4
  34. package/dist/language-ids.js +1 -1
  35. package/dist/line-text.d.ts +3 -0
  36. package/dist/line-text.js +7 -0
  37. package/dist/navigation.js +9 -9
  38. package/dist/paths.d.ts +1 -0
  39. package/dist/paths.js +4 -0
  40. package/dist/peek.js +11 -4
  41. package/dist/project-language.js +15 -13
  42. package/dist/project-problems.d.ts +1 -2
  43. package/dist/project-problems.js +2 -4
  44. package/dist/refresher.js +11 -6
  45. package/dist/rename.d.ts +2 -2
  46. package/dist/rename.js +14 -12
  47. package/dist/review-group.d.ts +1 -0
  48. package/dist/review-group.js +10 -14
  49. package/dist/row-host.d.ts +13 -20
  50. package/dist/row-host.js +33 -60
  51. package/dist/semantic-model.js +4 -6
  52. package/dist/semantic-tokens.js +2 -4
  53. package/dist/signature.js +1 -1
  54. package/dist/snippet-session.js +3 -4
  55. package/dist/symbol-links.js +2 -2
  56. package/dist/symbol-picker-model.js +15 -9
  57. package/dist/symbol-picker.js +2 -1
  58. package/dist/symbols.d.ts +7 -1
  59. package/dist/symbols.js +22 -28
  60. package/dist/testing.js +3 -3
  61. package/dist/word-diff.js +2 -4
  62. package/dist/workspace-edit.js +7 -8
  63. package/package.json +5 -5
  64. package/src/AnchoredPopup.tsx +2 -3
  65. package/src/HoverSections.tsx +5 -11
  66. package/src/PeekPanel.tsx +6 -14
  67. package/src/RenameCard.tsx +2 -2
  68. package/src/SymbolPicker.tsx +3 -4
  69. package/src/code-actions-model.ts +10 -8
  70. package/src/code-actions.ts +36 -38
  71. package/src/code-authors.ts +2 -2
  72. package/src/code-vision.ts +19 -15
  73. package/src/completion-call.ts +2 -6
  74. package/src/completion-model.ts +10 -12
  75. package/src/completion.ts +8 -13
  76. package/src/context-menu.ts +4 -5
  77. package/src/definition-link.ts +2 -1
  78. package/src/diagnostics-model.ts +10 -2
  79. package/src/diagnostics.ts +15 -13
  80. package/src/editor-language.ts +11 -11
  81. package/src/error-message.ts +3 -0
  82. package/src/folding.ts +6 -26
  83. package/src/history.ts +11 -12
  84. package/src/hover-content.ts +6 -4
  85. package/src/hover.ts +10 -9
  86. package/src/inlay-hints.ts +4 -4
  87. package/src/language-ids.ts +1 -1
  88. package/src/line-text.ts +10 -0
  89. package/src/navigation.ts +11 -10
  90. package/src/paths.ts +5 -0
  91. package/src/peek.ts +11 -10
  92. package/src/project-language.ts +17 -16
  93. package/src/project-problems.ts +3 -6
  94. package/src/refresher.ts +12 -6
  95. package/src/rename.ts +16 -18
  96. package/src/review-group.ts +11 -15
  97. package/src/row-host.ts +54 -76
  98. package/src/semantic-model.ts +4 -6
  99. package/src/semantic-tokens.ts +2 -4
  100. package/src/signature.ts +1 -1
  101. package/src/snippet-session.ts +3 -4
  102. package/src/symbol-links.ts +2 -2
  103. package/src/symbol-picker-model.ts +17 -9
  104. package/src/symbol-picker.ts +2 -1
  105. package/src/symbols.ts +29 -28
  106. package/src/testing.ts +4 -2
  107. package/src/view-state.ts +2 -0
  108. package/src/word-diff.ts +2 -4
  109. package/src/workspace-edit.ts +7 -8
@@ -4,7 +4,6 @@ export interface ReviewMember {
4
4
  hide(runIds: readonly string[]): void;
5
5
  /* Restores runs after a refused operation. */
6
6
  show(runIds: readonly string[]): void;
7
- /* Refreshes runs after an operation. */
8
7
  refresh(): void;
9
8
  }
10
9
 
@@ -24,20 +23,21 @@ export class ReviewGroup {
24
23
  }
25
24
 
26
25
  hide(runIds: readonly string[]): void {
27
- for (const member of [...this.members]) {
28
- member.hide(runIds);
29
- }
26
+ this.each((member) => member.hide(runIds));
30
27
  }
31
28
 
32
29
  show(runIds: readonly string[]): void {
33
- for (const member of [...this.members]) {
34
- member.show(runIds);
35
- }
30
+ this.each((member) => member.show(runIds));
36
31
  }
37
32
 
38
33
  refresh(): void {
34
+ this.each((member) => member.refresh());
35
+ }
36
+
37
+ /* Over a copy, since a member may leave while it hears. */
38
+ private each(action: (member: ReviewMember) => void): void {
39
39
  for (const member of [...this.members]) {
40
- member.refresh();
40
+ action(member);
41
41
  }
42
42
  }
43
43
  }
@@ -46,13 +46,9 @@ const groups = new Map<string, { group: ReviewGroup; holders: number }>();
46
46
 
47
47
  /* The host supplies a shared file key; the group is released with its last editor. */
48
48
  export function joinReviewGroup(key: string): { group: ReviewGroup; leave(): void } {
49
- let entry = groups.get(key);
50
- if (entry === undefined) {
51
- entry = { group: new ReviewGroup(), holders: 0 };
52
- groups.set(key, entry);
53
- }
54
- entry.holders++;
55
- const held = entry;
49
+ const held = groups.get(key) ?? { group: new ReviewGroup(), holders: 0 };
50
+ groups.set(key, held);
51
+ held.holders++;
56
52
  let left = false;
57
53
  return {
58
54
  group: held.group,
package/src/row-host.ts CHANGED
@@ -8,14 +8,16 @@ export interface HostedRow {
8
8
  height: number;
9
9
  }
10
10
 
11
- /*
12
- * The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
13
- * into them. The editor makes an element again whenever its row scrolls back into view, which is why a
14
- * row's content never keeps state of its own.
15
- */
16
- export class RowHost {
17
- private readonly editor: Editor;
18
- private readonly owner: string;
11
+ export interface HostedAction {
12
+ id: string;
13
+ /* Zero-based. */
14
+ line: number;
15
+ }
16
+
17
+ /* The elements an editor made for one owner's entries, and a version React reads them by. */
18
+ abstract class ContainerHost {
19
+ protected readonly editor: Editor;
20
+ protected readonly owner: string;
19
21
  private readonly containers = new Map<string, HTMLElement>();
20
22
  private readonly listeners = new Set<() => void>();
21
23
  private version = 0;
@@ -39,19 +41,50 @@ export class RowHost {
39
41
  return this.containers.get(id);
40
42
  }
41
43
 
42
- /* Sets the owner's rows; the same rows again are left alone, so an editor does not measure them twice. */
43
- set(rows: readonly HostedRow[]): void {
44
- const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
44
+ /* False for the same entries again, so an editor does not measure them twice; otherwise drops the elements of entries that left. */
45
+ protected replace(signature: string, ids: readonly string[]): boolean {
45
46
  if (signature === this.signature) {
46
- return;
47
+ return false;
47
48
  }
48
49
  this.signature = signature;
49
- const kept = new Set(rows.map((row) => row.id));
50
+ const kept = new Set(ids);
50
51
  for (const id of [...this.containers.keys()]) {
51
52
  if (!kept.has(id)) {
52
53
  this.containers.delete(id);
53
54
  }
54
55
  }
56
+ return true;
57
+ }
58
+
59
+ protected mounted(id: string, container: HTMLElement): void {
60
+ this.containers.set(id, container);
61
+ this.bump();
62
+ }
63
+
64
+ protected bump(): void {
65
+ this.version++;
66
+ for (const listener of [...this.listeners]) {
67
+ listener();
68
+ }
69
+ }
70
+ }
71
+
72
+ /*
73
+ * The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
74
+ * into them. The editor makes an element again whenever its row scrolls back into view, which is why a
75
+ * row's content never keeps state of its own.
76
+ */
77
+ export class RowHost extends ContainerHost {
78
+ set(rows: readonly HostedRow[]): void {
79
+ const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
80
+ if (
81
+ !this.replace(
82
+ signature,
83
+ rows.map((row) => row.id)
84
+ )
85
+ ) {
86
+ return;
87
+ }
55
88
  this.editor.setWidgets(
56
89
  rows.map((row) => ({
57
90
  id: row.id,
@@ -68,24 +101,6 @@ export class RowHost {
68
101
  clear(): void {
69
102
  this.set([]);
70
103
  }
71
-
72
- private mounted(id: string, container: HTMLElement): void {
73
- this.containers.set(id, container);
74
- this.bump();
75
- }
76
-
77
- private bump(): void {
78
- this.version++;
79
- for (const listener of [...this.listeners]) {
80
- listener();
81
- }
82
- }
83
- }
84
-
85
- export interface HostedAction {
86
- id: string;
87
- /* Zero-based. */
88
- line: number;
89
104
  }
90
105
 
91
106
  /*
@@ -93,52 +108,22 @@ export interface HostedAction {
93
108
  * them. An element lives as long as its action does, unlike a row's, so what React draws into it keeps
94
109
  * its state.
95
110
  */
96
- export class LineActionHost {
97
- private readonly editor: Editor;
98
- private readonly owner: string;
99
- private readonly containers = new Map<string, HTMLElement>();
100
- private readonly listeners = new Set<() => void>();
101
- private version = 0;
102
- private signature = '';
103
-
104
- constructor(editor: Editor, owner: string) {
105
- this.editor = editor;
106
- this.owner = owner;
107
- }
108
-
109
- getVersion = (): number => this.version;
110
-
111
- subscribe = (listener: () => void): (() => void) => {
112
- this.listeners.add(listener);
113
- return () => {
114
- this.listeners.delete(listener);
115
- };
116
- };
117
-
118
- container(id: string): HTMLElement | undefined {
119
- return this.containers.get(id);
120
- }
121
-
111
+ export class LineActionHost extends ContainerHost {
122
112
  set(actions: readonly HostedAction[]): void {
123
113
  const signature = actions.map((action) => `${action.id}@${action.line}`).join('|');
124
- if (signature === this.signature) {
114
+ if (
115
+ !this.replace(
116
+ signature,
117
+ actions.map((action) => action.id)
118
+ )
119
+ ) {
125
120
  return;
126
121
  }
127
- this.signature = signature;
128
- const kept = new Set(actions.map((action) => action.id));
129
- for (const id of [...this.containers.keys()]) {
130
- if (!kept.has(id)) {
131
- this.containers.delete(id);
132
- }
133
- }
134
122
  this.editor.setLineActions(
135
123
  actions.map((action) => ({
136
124
  id: action.id,
137
125
  line: action.line,
138
- render: (container: HTMLElement) => {
139
- this.containers.set(action.id, container);
140
- this.bump();
141
- }
126
+ render: (container: HTMLElement) => this.mounted(action.id, container)
142
127
  })),
143
128
  this.owner
144
129
  );
@@ -148,11 +133,4 @@ export class LineActionHost {
148
133
  clear(): void {
149
134
  this.set([]);
150
135
  }
151
-
152
- private bump(): void {
153
- this.version++;
154
- for (const listener of [...this.listeners]) {
155
- listener();
156
- }
157
- }
158
136
  }
@@ -2,12 +2,10 @@ import type { EditorSemanticToken } from '@adecore/editor';
2
2
  import type { SemanticTokens, SemanticTokensLegend } from '@adecore/lsp';
3
3
 
4
4
  /*
5
- * The TextMate scopes a classification stands for, so the editor's theme colors it the way its own
6
- * grammar colors the same thing. Null where the grammar already did better: a keyword, a string, a
7
- * local variable. A function being called, a method, a static member and a declaration are scopes of
8
- * their own, as the platform's color scheme tells them apart. `scoped` says the server marks what is
9
- * local to a function, so a variable it leaves unmarked lives at the top of a file or comes from a
10
- * library, which the scheme draws as a global.
5
+ * The TextMate scopes a classification stands for, so the theme colors it as its grammar would; null where
6
+ * the grammar already did better (a keyword, a string, a local variable). Calls, methods, statics and
7
+ * declarations get scopes of their own, as the color scheme tells them apart. `scoped` says the server
8
+ * marks what is local to a function, so a variable it leaves unmarked is drawn as a global.
11
9
  */
12
10
  export function scopesOf(type: string, modifiers: ReadonlySet<string>, scoped = false): string[] | null {
13
11
  switch (type) {
@@ -7,10 +7,8 @@ const METHOD = 'textDocument/semanticTokens/full';
7
7
  const PAUSE_MS = 200;
8
8
 
9
9
  /*
10
- * Colors the editor draws from what the language servers know, over what the grammar made of the text:
11
- * a class is a class and a call is a call where the grammar could only guess. It asks after a pause in
12
- * typing and when a server says its offer changed, and keeps drawing the last answer, which follows its
13
- * text through edits, in between, so the colors never blink out while the next one is on its way.
10
+ * Colors from what the language servers know, over the grammar's guess. It asks after a pause in typing
11
+ * and when a server's offer changed, and keeps drawing the last answer meanwhile so the colors never blink out.
14
12
  */
15
13
  export class SemanticTokensFeature {
16
14
  private readonly refresher: Refresher;
package/src/signature.ts CHANGED
@@ -67,7 +67,7 @@ export class SignatureFeature {
67
67
  this.ask({ triggerKind: 2, triggerCharacter: '(', isRetrigger: false });
68
68
  }
69
69
 
70
- /* Shows the next overload, or the one before, while the card is up and has more than one; false when it has not, which leaves the arrow to the caret. */
70
+ /* Shows the next or previous overload; false when the card has fewer than two, which leaves the arrow to the caret. */
71
71
  private cycle(step: 1 | -1): boolean {
72
72
  const { help } = this;
73
73
  if (help === null || help.signatures.length < 2 || !this.isOpen) {
@@ -12,10 +12,9 @@ export function positionInInsertion(start: EditorPosition, text: string, offset:
12
12
  }
13
13
 
14
14
  /*
15
- * The tab stops of a snippet that was just inserted. The caret goes to the first one, with its default text
16
- * selected; Tab goes on to the next and Shift+Tab back, and the last stop (`$0`, or the end of the text)
17
- * ends it. Escape, a caret that leaves the snippet and any change that is not typing end it too. Typing in
18
- * a stop does not touch the others: they follow the text, but a mirror of a stop is not edited along.
15
+ * The tab stops of a snippet that was just inserted. Tab and Shift+Tab move between them; the last stop,
16
+ * Escape, a caret that leaves the snippet and any change that is not typing end it. A mirror of a stop
17
+ * follows the text but is not edited along.
19
18
  */
20
19
  export class SnippetFeature {
21
20
  private readonly language: EditorLanguage;
@@ -3,9 +3,9 @@ import type { Location, SymbolInformation, WorkspaceSymbol, WorkspaceSymbolResul
3
3
  /* The classes a name in a signature has: a pointer and an underline on hover, and the accent only then, so the signature stays calm. */
4
4
  export const SYMBOL_LINK_CLASS = 'cursor-pointer hover:text-accent hover:underline';
5
5
 
6
- /* A name that reads as a type: it starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
7
6
  const TYPE_NAME = /^[A-Z][A-Za-z0-9_]*$/;
8
7
 
8
+ /* A name that starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
9
9
  export function isTypeName(name: string): boolean {
10
10
  return TYPE_NAME.test(name) && /[a-z]/.test(name);
11
11
  }
@@ -27,7 +27,7 @@ export function declaredNameOf(code: string): string | null {
27
27
  return DECLARES.exec(code)?.[1] ?? null;
28
28
  }
29
29
 
30
- /* What a workspace symbol is that a type name can mean. */
30
+ /* The symbol kinds a type name can mean: module, namespace, class, enum, interface, struct and type parameter. */
31
31
  const TYPE_KINDS = new Set([2, 3, 5, 10, 11, 23, 26]);
32
32
 
33
33
  interface Found {
@@ -40,6 +40,8 @@ export function groupOfKind(kind: number): SymbolGroup {
40
40
  return GROUP_OF_KIND[kind] ?? 'other';
41
41
  }
42
42
 
43
+ const ENUM_MEMBER = 22;
44
+
43
45
  const LETTERS: Record<SymbolGroup, string> = {
44
46
  namespaces: 'N',
45
47
  classes: 'C',
@@ -55,20 +57,26 @@ const LETTERS: Record<SymbolGroup, string> = {
55
57
 
56
58
  /* The letter in the badge in front of a symbol. */
57
59
  export function letterOfKind(kind: number): string {
58
- return kind === 22 ? 'e' : LETTERS[groupOfKind(kind)];
60
+ return kind === ENUM_MEMBER ? 'e' : LETTERS[groupOfKind(kind)];
59
61
  }
60
62
 
61
63
  export type SymbolTone = 'callable' | 'value' | 'type' | 'other';
62
64
 
65
+ const TONES: Record<SymbolGroup, SymbolTone> = {
66
+ namespaces: 'other',
67
+ classes: 'type',
68
+ interfaces: 'type',
69
+ enums: 'type',
70
+ functions: 'callable',
71
+ methods: 'callable',
72
+ properties: 'value',
73
+ variables: 'value',
74
+ constants: 'value',
75
+ other: 'other'
76
+ };
77
+
63
78
  export function toneOfKind(kind: number): SymbolTone {
64
- const group = groupOfKind(kind);
65
- return group === 'functions' || group === 'methods'
66
- ? 'callable'
67
- : group === 'classes' || group === 'interfaces' || group === 'enums'
68
- ? 'type'
69
- : group === 'properties' || group === 'variables' || group === 'constants'
70
- ? 'value'
71
- : 'other';
79
+ return TONES[groupOfKind(kind)];
72
80
  }
73
81
 
74
82
  /* One symbol of a file as a row of the picker. */
@@ -3,6 +3,7 @@ import { basenameOf } from './paths.ts';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { entriesOf, workspaceEntriesOf, type SymbolEntry, type WorkspaceEntry } from './symbol-picker-model.ts';
5
5
  import { isShortcut } from './shortcut-keys.ts';
6
+ import { messageOf } from './error-message.ts';
6
7
 
7
8
  const TOAST_ID = 'language-symbols';
8
9
 
@@ -43,7 +44,7 @@ export class SymbolPickerFeature {
43
44
  this.language.project.host.notify?.({
44
45
  id: TOAST_ID,
45
46
  kind: 'error',
46
- title: this.language.project.i18n.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
47
+ title: this.language.project.i18n.t('editor:language.symbols.failed', { message: messageOf(error) })
47
48
  });
48
49
  }
49
50
  }
package/src/symbols.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EditorBlock } from '@adecore/editor';
2
- import type { DocumentSymbol, DocumentSymbolResult, SymbolInformation } from '@adecore/lsp';
2
+ import type { DocumentSymbol, DocumentSymbolResult, Range, SymbolInformation } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { Refresher } from './refresher.ts';
5
5
  import { realTimers, type Timers } from './timers.ts';
@@ -23,42 +23,43 @@ const KINDS: Record<number, string> = {
23
23
  23: 'struct'
24
24
  };
25
25
 
26
- function isHierarchical(symbols: readonly (DocumentSymbol | SymbolInformation)[]): symbols is DocumentSymbol[] {
27
- return symbols.length > 0 && 'range' in symbols[0]!;
26
+ export interface SymbolSpan {
27
+ readonly name: string;
28
+ readonly kind: number;
29
+ readonly range: Range;
28
30
  }
29
31
 
30
- function blockOf(name: string, kind: number, startLine: number, endLine: number): EditorBlock | null {
31
- const label = KINDS[kind];
32
- // A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
33
- return label === undefined || endLine <= startLine ? null : { startLine: startLine + 1, endLine: endLine + 1, name, kind: label };
34
- }
35
-
36
- function walk(symbols: readonly DocumentSymbol[], blocks: EditorBlock[]): void {
32
+ function walk(symbols: readonly DocumentSymbol[], spans: SymbolSpan[]): void {
37
33
  for (const symbol of symbols) {
38
- const block = blockOf(symbol.name, symbol.kind, symbol.range.start.line, symbol.range.end.line);
39
- if (block !== null) {
40
- blocks.push(block);
41
- }
42
- walk(symbol.children ?? [], blocks);
34
+ spans.push({ name: symbol.name, kind: symbol.kind, range: symbol.range });
35
+ walk(symbol.children ?? [], spans);
43
36
  }
44
37
  }
45
38
 
46
- /* The multi-line symbols of a file as blocks, outermost first, in one-based lines. */
47
- export function blocksOf(result: DocumentSymbolResult): EditorBlock[] {
48
- const blocks: EditorBlock[] = [];
39
+ /* Every symbol of an answer, a parent before its children, whether the server sent a tree or a flat list. */
40
+ export function symbolSpansOf(result: DocumentSymbolResult): SymbolSpan[] {
41
+ const spans: SymbolSpan[] = [];
49
42
  if (result === null || result.length === 0) {
50
- return blocks;
43
+ return spans;
51
44
  }
52
- if (isHierarchical(result)) {
53
- walk(result, blocks);
54
- } else {
55
- for (const symbol of result as SymbolInformation[]) {
56
- const block = blockOf(symbol.name, symbol.kind, symbol.location.range.start.line, symbol.location.range.end.line);
57
- if (block !== null) {
58
- blocks.push(block);
59
- }
60
- }
45
+ if ('range' in result[0]!) {
46
+ walk(result as DocumentSymbol[], spans);
47
+ return spans;
61
48
  }
49
+ return (result as SymbolInformation[]).map((symbol) => ({ name: symbol.name, kind: symbol.kind, range: symbol.location.range }));
50
+ }
51
+
52
+ function blockOf({ name, kind, range }: SymbolSpan): EditorBlock | null {
53
+ const label = KINDS[kind];
54
+ // A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
55
+ return label === undefined || range.end.line <= range.start.line
56
+ ? null
57
+ : { startLine: range.start.line + 1, endLine: range.end.line + 1, name, kind: label };
58
+ }
59
+
60
+ /* The multi-line symbols of a file as blocks, outermost first, in one-based lines. */
61
+ export function blocksOf(result: DocumentSymbolResult): EditorBlock[] {
62
+ const blocks = symbolSpansOf(result).flatMap((span) => blockOf(span) ?? []);
62
63
  return blocks.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
63
64
  }
64
65
 
package/src/testing.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  applyContentChanges,
3
+ ErrorCodes,
3
4
  LspError,
4
5
  StaleResultError,
5
6
  type LanguageService,
@@ -69,7 +70,7 @@ export class FakeLanguageService implements LanguageService {
69
70
  async changeDocument(uri: string, changes: readonly ContentChange[]): Promise<void> {
70
71
  const held = this.documents.get(uri);
71
72
  if (held === undefined) {
72
- throw new LspError('Document is not open', -32002);
73
+ throw new LspError('Document is not open', ErrorCodes.ServerNotInitialized);
73
74
  }
74
75
  held.text = applyContentChanges(held.text, changes);
75
76
  held.version++;
@@ -113,7 +114,7 @@ export class FakeLanguageService implements LanguageService {
113
114
 
114
115
  private async request<Result>(method: string, uri: string, params: unknown, options: LanguageRequestOptions = {}): Promise<Result> {
115
116
  if (!this.supports(method, uri)) {
116
- throw new LspError(`${method} is unavailable`, -32601);
117
+ throw new LspError(`${method} is unavailable`, ErrorCodes.MethodNotFound);
117
118
  }
118
119
  options.signal?.throwIfAborted();
119
120
  const version = this.documents.get(uri)?.version;
@@ -126,6 +127,7 @@ export class FakeLanguageService implements LanguageService {
126
127
  }
127
128
  return result as Result;
128
129
  }
130
+
129
131
  completion(uri: string, position: Position, context?: CompletionContext, options: LanguageRequestOptions = {}): Promise<CompletionResult> {
130
132
  return this.request<CompletionResult>('textDocument/completion', uri, { position, context }, options);
131
133
  }
package/src/view-state.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { EditorFolds, FoldRole } from '@adecore/editor';
2
+
2
3
  export interface RevealLineRequest {
3
4
  readonly line: number;
4
5
  }
6
+
5
7
  function endpointKey(namespace: string, path: string): string {
6
8
  return `${namespace}:${path}`;
7
9
  }
package/src/word-diff.ts CHANGED
@@ -4,10 +4,8 @@ import { diffLines } from '@adecore/merge';
4
4
  const TOKEN = /\w+|\s+|[^\w\s]/g;
5
5
 
6
6
  /*
7
- * The character ranges of each removed line that the added lines changed, `[start, end)` per line, for
8
- * the stronger tint on the words a change replaced. Lines are only compared when as many were added as
9
- * removed, since then each removed line has the line it became; any other change has no such pairing
10
- * and draws no words.
7
+ * The `[start, end)` ranges of each removed line that the added lines replaced. Lines pair up only when as
8
+ * many were added as removed; any other change has no pairing and marks no words.
11
9
  */
12
10
  export function replacedWords(removed: readonly string[], added: readonly string[]): Array<Array<[number, number]>> {
13
11
  return removed.map((line, index) => (removed.length === added.length ? replacedRanges(line, added[index]!) : []));
@@ -9,6 +9,7 @@ import {
9
9
  type WorkspaceEdit
10
10
  } from '@adecore/lsp';
11
11
  import type { DiskText } from './host-types.ts';
12
+ import { messageOf } from './error-message.ts';
12
13
 
13
14
  /* A file that changed without an editor of this project holding it: its new text waits as an unsaved draft. */
14
15
  export interface StagedFile {
@@ -113,12 +114,10 @@ interface PlannedCreate {
113
114
  type Planned = PlannedEdits | PlannedCreate | { readonly kind: 'rename'; readonly oldPath: string; readonly newPath: string };
114
115
 
115
116
  /*
116
- * Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
117
- * its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
118
- * nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
119
- * move are then one change, so every file is saved through the machine and the move is made there. A file
120
- * the edit creates is made through the machine with the text the edit writes into it. The edit is checked
121
- * against every text first, so one that does not fit changes nothing.
117
+ * Applies an LSP workspace edit step by step, after checking it against every text so one that does not fit
118
+ * changes nothing. A file an editor holds takes its edits as one undo step; any other file gets an unsaved
119
+ * draft, since nothing is written to disk on a server's word alone, unless the edit also moves a file: then it
120
+ * is one change, saved and moved through the machine. A file the edit creates is made through the machine.
122
121
  */
123
122
  export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult> {
124
123
  const steps = stepsOf(edit);
@@ -206,7 +205,7 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
206
205
  try {
207
206
  created.text = applyTextEdits(created.text, step.edits);
208
207
  } catch (error) {
209
- return error instanceof Error ? error.message : String(error);
208
+ return messageOf(error);
210
209
  }
211
210
  continue;
212
211
  }
@@ -228,7 +227,7 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
228
227
  try {
229
228
  after = applyTextEdits(held.text, step.edits);
230
229
  } catch (error) {
231
- return error instanceof Error ? error.message : String(error);
230
+ return messageOf(error);
232
231
  }
233
232
  planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
234
233
  texts.set(source, { text: after, disk: held.disk });