@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
package/dist/rename.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { StaleResultError } from '@adecore/lsp';
2
+ import { lineTextOf } from './line-text.js';
2
3
  import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt } from './rename-model.js';
3
4
  import { isShortcut } from './shortcut-keys.js';
5
+ import { messageOf } from './error-message.js';
4
6
  const METHOD = 'textDocument/rename';
5
7
  const TOAST_ID = 'language-rename';
6
- /* How many names are offered, and how many other lines and files are read to say how the symbol is used. */
8
+ /* How much is read to tell a name suggester how the symbol is used: its first places, other files and lines around it. */
7
9
  const SAMPLE_LOCATIONS = 8;
8
10
  const SAMPLE_OTHER_FILES = 3;
9
11
  const CONTEXT_LINES = 3;
@@ -13,9 +15,6 @@ const CONTEXT_LINES = 3;
13
15
  * Edits to other files land as unsaved drafts (`applyWorkspaceEdit`), so nothing is written to disk here.
14
16
  */
15
17
  export class RenameFeature {
16
- tell(kind, title) {
17
- this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
18
- }
19
18
  language;
20
19
  session = null;
21
20
  constructor(language) {
@@ -60,7 +59,7 @@ export class RenameFeature {
60
59
  }
61
60
  return;
62
61
  }
63
- const target = renameTargetOf(answer, (range) => editor.textInRange(range), () => wordRangeAt(editor.textInRange({ start: { line: caret.line, character: 0 }, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } }), caret));
62
+ const target = renameTargetOf(answer, (range) => editor.textInRange(range), () => wordRangeAt(lineTextOf(editor, caret.line), caret));
64
63
  if (target === null) {
65
64
  this.tell('error', this.say('nothing'));
66
65
  return;
@@ -110,7 +109,7 @@ export class RenameFeature {
110
109
  }
111
110
  catch (error) {
112
111
  if (this.session === session) {
113
- this.fail(error instanceof StaleResultError ? this.say('stale') : error instanceof Error ? error.message : String(error));
112
+ this.fail(error instanceof StaleResultError ? this.say('stale') : messageOf(error));
114
113
  }
115
114
  }
116
115
  }
@@ -127,6 +126,12 @@ export class RenameFeature {
127
126
  }
128
127
  this.finish();
129
128
  }
129
+ /* The name was changed, which answers the failure under it. */
130
+ edited() {
131
+ if (this.language.popups.getState().rename?.error != null) {
132
+ this.publish({ error: null });
133
+ }
134
+ }
130
135
  async apply(session) {
131
136
  const result = await this.language.project.applyWorkspaceEdit(session.edit);
132
137
  if (this.session !== session) {
@@ -146,12 +151,6 @@ export class RenameFeature {
146
151
  this.publish({ phase: 'input', busy: false, files: [], error: this.say('failed', { message }) });
147
152
  }
148
153
  }
149
- /* The name was changed, which answers the failure under it. */
150
- edited() {
151
- if (this.language.popups.getState().rename?.error != null) {
152
- this.publish({ error: null });
153
- }
154
- }
155
154
  finish() {
156
155
  this.session?.names.abort();
157
156
  this.session = null;
@@ -251,6 +250,9 @@ export class RenameFeature {
251
250
  }
252
251
  });
253
252
  }
253
+ tell(kind, title) {
254
+ this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
255
+ }
254
256
  say(key, options) {
255
257
  return this.language.project.i18n.t(`editor:language.rename.${key}`, options);
256
258
  }
@@ -9,6 +9,7 @@ export declare class ReviewGroup {
9
9
  hide(runIds: readonly string[]): void;
10
10
  show(runIds: readonly string[]): void;
11
11
  refresh(): void;
12
+ private each;
12
13
  }
13
14
  export declare function joinReviewGroup(key: string): {
14
15
  group: ReviewGroup;
@@ -12,31 +12,27 @@ export class ReviewGroup {
12
12
  };
13
13
  }
14
14
  hide(runIds) {
15
- for (const member of [...this.members]) {
16
- member.hide(runIds);
17
- }
15
+ this.each((member) => member.hide(runIds));
18
16
  }
19
17
  show(runIds) {
20
- for (const member of [...this.members]) {
21
- member.show(runIds);
22
- }
18
+ this.each((member) => member.show(runIds));
23
19
  }
24
20
  refresh() {
21
+ this.each((member) => member.refresh());
22
+ }
23
+ /* Over a copy, since a member may leave while it hears. */
24
+ each(action) {
25
25
  for (const member of [...this.members]) {
26
- member.refresh();
26
+ action(member);
27
27
  }
28
28
  }
29
29
  }
30
30
  const groups = new Map();
31
31
  /* The host supplies a shared file key; the group is released with its last editor. */
32
32
  export function joinReviewGroup(key) {
33
- let entry = groups.get(key);
34
- if (entry === undefined) {
35
- entry = { group: new ReviewGroup(), holders: 0 };
36
- groups.set(key, entry);
37
- }
38
- entry.holders++;
39
- const held = entry;
33
+ const held = groups.get(key) ?? { group: new ReviewGroup(), holders: 0 };
34
+ groups.set(key, held);
35
+ held.holders++;
40
36
  let left = false;
41
37
  return {
42
38
  group: held.group,
@@ -5,29 +5,13 @@ export interface HostedRow {
5
5
  placement: 'above' | 'below';
6
6
  height: number;
7
7
  }
8
- export declare class RowHost {
9
- private readonly editor;
10
- private readonly owner;
11
- private readonly containers;
12
- private readonly listeners;
13
- private version;
14
- private signature;
15
- constructor(editor: Editor, owner: string);
16
- getVersion: () => number;
17
- subscribe: (listener: () => void) => (() => void);
18
- container(id: string): HTMLElement | undefined;
19
- set(rows: readonly HostedRow[]): void;
20
- clear(): void;
21
- private mounted;
22
- private bump;
23
- }
24
8
  export interface HostedAction {
25
9
  id: string;
26
10
  line: number;
27
11
  }
28
- export declare class LineActionHost {
29
- private readonly editor;
30
- private readonly owner;
12
+ declare abstract class ContainerHost {
13
+ protected readonly editor: Editor;
14
+ protected readonly owner: string;
31
15
  private readonly containers;
32
16
  private readonly listeners;
33
17
  private version;
@@ -36,7 +20,16 @@ export declare class LineActionHost {
36
20
  getVersion: () => number;
37
21
  subscribe: (listener: () => void) => (() => void);
38
22
  container(id: string): HTMLElement | undefined;
23
+ protected replace(signature: string, ids: readonly string[]): boolean;
24
+ protected mounted(id: string, container: HTMLElement): void;
25
+ protected bump(): void;
26
+ }
27
+ export declare class RowHost extends ContainerHost {
28
+ set(rows: readonly HostedRow[]): void;
29
+ clear(): void;
30
+ }
31
+ export declare class LineActionHost extends ContainerHost {
39
32
  set(actions: readonly HostedAction[]): void;
40
33
  clear(): void;
41
- private bump;
42
34
  }
35
+ export {};
package/dist/row-host.js CHANGED
@@ -1,9 +1,5 @@
1
- /*
2
- * The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
3
- * into them. The editor makes an element again whenever its row scrolls back into view, which is why a
4
- * row's content never keeps state of its own.
5
- */
6
- export class RowHost {
1
+ /* The elements an editor made for one owner's entries, and a version React reads them by. */
2
+ class ContainerHost {
7
3
  editor;
8
4
  owner;
9
5
  containers = new Map();
@@ -24,19 +20,42 @@ export class RowHost {
24
20
  container(id) {
25
21
  return this.containers.get(id);
26
22
  }
27
- /* Sets the owner's rows; the same rows again are left alone, so an editor does not measure them twice. */
28
- set(rows) {
29
- const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
23
+ /* False for the same entries again, so an editor does not measure them twice; otherwise drops the elements of entries that left. */
24
+ replace(signature, ids) {
30
25
  if (signature === this.signature) {
31
- return;
26
+ return false;
32
27
  }
33
28
  this.signature = signature;
34
- const kept = new Set(rows.map((row) => row.id));
29
+ const kept = new Set(ids);
35
30
  for (const id of [...this.containers.keys()]) {
36
31
  if (!kept.has(id)) {
37
32
  this.containers.delete(id);
38
33
  }
39
34
  }
35
+ return true;
36
+ }
37
+ mounted(id, container) {
38
+ this.containers.set(id, container);
39
+ this.bump();
40
+ }
41
+ bump() {
42
+ this.version++;
43
+ for (const listener of [...this.listeners]) {
44
+ listener();
45
+ }
46
+ }
47
+ }
48
+ /*
49
+ * The rows of one owner in an editor, and the elements the editor draws them in, so React can portal
50
+ * into them. The editor makes an element again whenever its row scrolls back into view, which is why a
51
+ * row's content never keeps state of its own.
52
+ */
53
+ export class RowHost extends ContainerHost {
54
+ set(rows) {
55
+ const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
56
+ if (!this.replace(signature, rows.map((row) => row.id))) {
57
+ return;
58
+ }
40
59
  this.editor.setWidgets(rows.map((row) => ({
41
60
  id: row.id,
42
61
  line: row.line,
@@ -49,72 +68,26 @@ export class RowHost {
49
68
  clear() {
50
69
  this.set([]);
51
70
  }
52
- mounted(id, container) {
53
- this.containers.set(id, container);
54
- this.bump();
55
- }
56
- bump() {
57
- this.version++;
58
- for (const listener of [...this.listeners]) {
59
- listener();
60
- }
61
- }
62
71
  }
63
72
  /*
64
73
  * The actions of one owner after the end of lines in an editor, and the elements the editor makes for
65
74
  * them. An element lives as long as its action does, unlike a row's, so what React draws into it keeps
66
75
  * its state.
67
76
  */
68
- export class LineActionHost {
69
- editor;
70
- owner;
71
- containers = new Map();
72
- listeners = new Set();
73
- version = 0;
74
- signature = '';
75
- constructor(editor, owner) {
76
- this.editor = editor;
77
- this.owner = owner;
78
- }
79
- getVersion = () => this.version;
80
- subscribe = (listener) => {
81
- this.listeners.add(listener);
82
- return () => {
83
- this.listeners.delete(listener);
84
- };
85
- };
86
- container(id) {
87
- return this.containers.get(id);
88
- }
77
+ export class LineActionHost extends ContainerHost {
89
78
  set(actions) {
90
79
  const signature = actions.map((action) => `${action.id}@${action.line}`).join('|');
91
- if (signature === this.signature) {
80
+ if (!this.replace(signature, actions.map((action) => action.id))) {
92
81
  return;
93
82
  }
94
- this.signature = signature;
95
- const kept = new Set(actions.map((action) => action.id));
96
- for (const id of [...this.containers.keys()]) {
97
- if (!kept.has(id)) {
98
- this.containers.delete(id);
99
- }
100
- }
101
83
  this.editor.setLineActions(actions.map((action) => ({
102
84
  id: action.id,
103
85
  line: action.line,
104
- render: (container) => {
105
- this.containers.set(action.id, container);
106
- this.bump();
107
- }
86
+ render: (container) => this.mounted(action.id, container)
108
87
  })), this.owner);
109
88
  this.bump();
110
89
  }
111
90
  clear() {
112
91
  this.set([]);
113
92
  }
114
- bump() {
115
- this.version++;
116
- for (const listener of [...this.listeners]) {
117
- listener();
118
- }
119
- }
120
93
  }
@@ -1,10 +1,8 @@
1
1
  /*
2
- * The TextMate scopes a classification stands for, so the editor's theme colors it the way its own
3
- * grammar colors the same thing. Null where the grammar already did better: a keyword, a string, a
4
- * local variable. A function being called, a method, a static member and a declaration are scopes of
5
- * their own, as the platform's color scheme tells them apart. `scoped` says the server marks what is
6
- * local to a function, so a variable it leaves unmarked lives at the top of a file or comes from a
7
- * library, which the scheme draws as a global.
2
+ * The TextMate scopes a classification stands for, so the theme colors it as its grammar would; null where
3
+ * the grammar already did better (a keyword, a string, a local variable). Calls, methods, statics and
4
+ * declarations get scopes of their own, as the color scheme tells them apart. `scoped` says the server
5
+ * marks what is local to a function, so a variable it leaves unmarked is drawn as a global.
8
6
  */
9
7
  export function scopesOf(type, modifiers, scoped = false) {
10
8
  switch (type) {
@@ -4,10 +4,8 @@ import { realTimers } from './timers.js';
4
4
  const METHOD = 'textDocument/semanticTokens/full';
5
5
  const PAUSE_MS = 200;
6
6
  /*
7
- * Colors the editor draws from what the language servers know, over what the grammar made of the text:
8
- * a class is a class and a call is a call where the grammar could only guess. It asks after a pause in
9
- * typing and when a server says its offer changed, and keeps drawing the last answer, which follows its
10
- * text through edits, in between, so the colors never blink out while the next one is on its way.
7
+ * Colors from what the language servers know, over the grammar's guess. It asks after a pause in typing
8
+ * and when a server's offer changed, and keeps drawing the last answer meanwhile so the colors never blink out.
11
9
  */
12
10
  export class SemanticTokensFeature {
13
11
  refresher;
package/dist/signature.js CHANGED
@@ -57,7 +57,7 @@ export class SignatureFeature {
57
57
  trigger() {
58
58
  this.ask({ triggerKind: 2, triggerCharacter: '(', isRetrigger: false });
59
59
  }
60
- /* 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. */
60
+ /* Shows the next or previous overload; false when the card has fewer than two, which leaves the arrow to the caret. */
61
61
  cycle(step) {
62
62
  const { help } = this;
63
63
  if (help === null || help.signatures.length < 2 || !this.isOpen) {
@@ -7,10 +7,9 @@ export function positionInInsertion(start, text, offset) {
7
7
  : { line: start.line + lines.length - 1, character: lines[lines.length - 1].length };
8
8
  }
9
9
  /*
10
- * The tab stops of a snippet that was just inserted. The caret goes to the first one, with its default text
11
- * selected; Tab goes on to the next and Shift+Tab back, and the last stop (`$0`, or the end of the text)
12
- * ends it. Escape, a caret that leaves the snippet and any change that is not typing end it too. Typing in
13
- * a stop does not touch the others: they follow the text, but a mirror of a stop is not edited along.
10
+ * The tab stops of a snippet that was just inserted. Tab and Shift+Tab move between them; the last stop,
11
+ * Escape, a caret that leaves the snippet and any change that is not typing end it. A mirror of a stop
12
+ * follows the text but is not edited along.
14
13
  */
15
14
  export class SnippetFeature {
16
15
  language;
@@ -1,7 +1,7 @@
1
1
  /* 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. */
2
2
  export const SYMBOL_LINK_CLASS = 'cursor-pointer hover:text-accent hover:underline';
3
- /* A name that reads as a type: it starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
4
3
  const TYPE_NAME = /^[A-Z][A-Za-z0-9_]*$/;
4
+ /* A name that starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
5
5
  export function isTypeName(name) {
6
6
  return TYPE_NAME.test(name) && /[a-z]/.test(name);
7
7
  }
@@ -17,7 +17,7 @@ const DECLARES = /\b(?:class|interface|enum|trait|type|namespace|struct|function
17
17
  export function declaredNameOf(code) {
18
18
  return DECLARES.exec(code)?.[1] ?? null;
19
19
  }
20
- /* What a workspace symbol is that a type name can mean. */
20
+ /* The symbol kinds a type name can mean: module, namespace, class, enum, interface, struct and type parameter. */
21
21
  const TYPE_KINDS = new Set([2, 3, 5, 10, 11, 23, 26]);
22
22
  /*
23
23
  * The places a type name leads to among the symbols a server found for it. Only symbols of exactly that name
@@ -32,6 +32,7 @@ const GROUP_OF_KIND = {
32
32
  export function groupOfKind(kind) {
33
33
  return GROUP_OF_KIND[kind] ?? 'other';
34
34
  }
35
+ const ENUM_MEMBER = 22;
35
36
  const LETTERS = {
36
37
  namespaces: 'N',
37
38
  classes: 'C',
@@ -46,17 +47,22 @@ const LETTERS = {
46
47
  };
47
48
  /* The letter in the badge in front of a symbol. */
48
49
  export function letterOfKind(kind) {
49
- return kind === 22 ? 'e' : LETTERS[groupOfKind(kind)];
50
+ return kind === ENUM_MEMBER ? 'e' : LETTERS[groupOfKind(kind)];
50
51
  }
52
+ const TONES = {
53
+ namespaces: 'other',
54
+ classes: 'type',
55
+ interfaces: 'type',
56
+ enums: 'type',
57
+ functions: 'callable',
58
+ methods: 'callable',
59
+ properties: 'value',
60
+ variables: 'value',
61
+ constants: 'value',
62
+ other: 'other'
63
+ };
51
64
  export function toneOfKind(kind) {
52
- const group = groupOfKind(kind);
53
- return group === 'functions' || group === 'methods'
54
- ? 'callable'
55
- : group === 'classes' || group === 'interfaces' || group === 'enums'
56
- ? 'type'
57
- : group === 'properties' || group === 'variables' || group === 'constants'
58
- ? 'value'
59
- : 'other';
65
+ return TONES[groupOfKind(kind)];
60
66
  }
61
67
  function isHierarchical(symbols) {
62
68
  return symbols.length > 0 && 'selectionRange' in symbols[0];
@@ -2,6 +2,7 @@ import { StaleResultError } from '@adecore/lsp';
2
2
  import { basenameOf } from './paths.js';
3
3
  import { entriesOf, workspaceEntriesOf } from './symbol-picker-model.js';
4
4
  import { isShortcut } from './shortcut-keys.js';
5
+ import { messageOf } from './error-message.js';
5
6
  const TOAST_ID = 'language-symbols';
6
7
  /*
7
8
  * Go to symbol: a picker over the symbols the language servers know in the file, filtered as it is typed.
@@ -39,7 +40,7 @@ export class SymbolPickerFeature {
39
40
  this.language.project.host.notify?.({
40
41
  id: TOAST_ID,
41
42
  kind: 'error',
42
- title: this.language.project.i18n.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
43
+ title: this.language.project.i18n.t('editor:language.symbols.failed', { message: messageOf(error) })
43
44
  });
44
45
  }
45
46
  }
package/dist/symbols.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import type { EditorBlock } from '@adecore/editor';
2
- import type { DocumentSymbolResult } from '@adecore/lsp';
2
+ import type { DocumentSymbolResult, Range } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { type Timers } from './timers.ts';
5
+ export interface SymbolSpan {
6
+ readonly name: string;
7
+ readonly kind: number;
8
+ readonly range: Range;
9
+ }
10
+ export declare function symbolSpansOf(result: DocumentSymbolResult): SymbolSpan[];
5
11
  export declare function blocksOf(result: DocumentSymbolResult): EditorBlock[];
6
12
  export declare class SymbolsFeature {
7
13
  private readonly refresher;
package/dist/symbols.js CHANGED
@@ -17,40 +17,34 @@ const KINDS = {
17
17
  14: 'constant',
18
18
  23: 'struct'
19
19
  };
20
- function isHierarchical(symbols) {
21
- return symbols.length > 0 && 'range' in symbols[0];
22
- }
23
- function blockOf(name, kind, startLine, endLine) {
24
- const label = KINDS[kind];
25
- // A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
26
- return label === undefined || endLine <= startLine ? null : { startLine: startLine + 1, endLine: endLine + 1, name, kind: label };
27
- }
28
- function walk(symbols, blocks) {
20
+ function walk(symbols, spans) {
29
21
  for (const symbol of symbols) {
30
- const block = blockOf(symbol.name, symbol.kind, symbol.range.start.line, symbol.range.end.line);
31
- if (block !== null) {
32
- blocks.push(block);
33
- }
34
- walk(symbol.children ?? [], blocks);
22
+ spans.push({ name: symbol.name, kind: symbol.kind, range: symbol.range });
23
+ walk(symbol.children ?? [], spans);
35
24
  }
36
25
  }
37
- /* The multi-line symbols of a file as blocks, outermost first, in one-based lines. */
38
- export function blocksOf(result) {
39
- const blocks = [];
26
+ /* Every symbol of an answer, a parent before its children, whether the server sent a tree or a flat list. */
27
+ export function symbolSpansOf(result) {
28
+ const spans = [];
40
29
  if (result === null || result.length === 0) {
41
- return blocks;
42
- }
43
- if (isHierarchical(result)) {
44
- walk(result, blocks);
30
+ return spans;
45
31
  }
46
- else {
47
- for (const symbol of result) {
48
- const block = blockOf(symbol.name, symbol.kind, symbol.location.range.start.line, symbol.location.range.end.line);
49
- if (block !== null) {
50
- blocks.push(block);
51
- }
52
- }
32
+ if ('range' in result[0]) {
33
+ walk(result, spans);
34
+ return spans;
53
35
  }
36
+ return result.map((symbol) => ({ name: symbol.name, kind: symbol.kind, range: symbol.location.range }));
37
+ }
38
+ function blockOf({ name, kind, range }) {
39
+ const label = KINDS[kind];
40
+ // A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
41
+ return label === undefined || range.end.line <= range.start.line
42
+ ? null
43
+ : { startLine: range.start.line + 1, endLine: range.end.line + 1, name, kind: label };
44
+ }
45
+ /* The multi-line symbols of a file as blocks, outermost first, in one-based lines. */
46
+ export function blocksOf(result) {
47
+ const blocks = symbolSpansOf(result).flatMap((span) => blockOf(span) ?? []);
54
48
  return blocks.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
55
49
  }
56
50
  /*
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { applyContentChanges, LspError, StaleResultError } from '@adecore/lsp';
1
+ import { applyContentChanges, ErrorCodes, LspError, StaleResultError } from '@adecore/lsp';
2
2
  export { ManualTimers } from './timers.js';
3
3
  export class FakeLanguageService {
4
4
  documents = new Map();
@@ -20,7 +20,7 @@ export class FakeLanguageService {
20
20
  async changeDocument(uri, changes) {
21
21
  const held = this.documents.get(uri);
22
22
  if (held === undefined) {
23
- throw new LspError('Document is not open', -32002);
23
+ throw new LspError('Document is not open', ErrorCodes.ServerNotInitialized);
24
24
  }
25
25
  held.text = applyContentChanges(held.text, changes);
26
26
  held.version++;
@@ -56,7 +56,7 @@ export class FakeLanguageService {
56
56
  }
57
57
  async request(method, uri, params, options = {}) {
58
58
  if (!this.supports(method, uri)) {
59
- throw new LspError(`${method} is unavailable`, -32601);
59
+ throw new LspError(`${method} is unavailable`, ErrorCodes.MethodNotFound);
60
60
  }
61
61
  options.signal?.throwIfAborted();
62
62
  const version = this.documents.get(uri)?.version;
package/dist/word-diff.js CHANGED
@@ -2,10 +2,8 @@ import { diffLines } from '@adecore/merge';
2
2
  /* A word, a run of spaces or one other character, so a change inside a line is found by word and not by letter. */
3
3
  const TOKEN = /\w+|\s+|[^\w\s]/g;
4
4
  /*
5
- * The character ranges of each removed line that the added lines changed, `[start, end)` per line, for
6
- * the stronger tint on the words a change replaced. Lines are only compared when as many were added as
7
- * removed, since then each removed line has the line it became; any other change has no such pairing
8
- * and draws no words.
5
+ * The `[start, end)` ranges of each removed line that the added lines replaced. Lines pair up only when as
6
+ * many were added as removed; any other change has no pairing and marks no words.
9
7
  */
10
8
  export function replacedWords(removed, added) {
11
9
  return removed.map((line, index) => (removed.length === added.length ? replacedRanges(line, added[index]) : []));
@@ -1,4 +1,5 @@
1
1
  import { applyTextEdits, fileUriToPath } from '@adecore/lsp';
2
+ import { messageOf } from './error-message.js';
2
3
  function refused(failureReason) {
3
4
  return { applied: false, failureReason };
4
5
  }
@@ -48,12 +49,10 @@ function contentChangeOf(edit) {
48
49
  return { range: edit.range, text: edit.newText };
49
50
  }
50
51
  /*
51
- * Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
52
- * its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
53
- * nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
54
- * move are then one change, so every file is saved through the machine and the move is made there. A file
55
- * the edit creates is made through the machine with the text the edit writes into it. The edit is checked
56
- * against every text first, so one that does not fit changes nothing.
52
+ * Applies an LSP workspace edit step by step, after checking it against every text so one that does not fit
53
+ * changes nothing. A file an editor holds takes its edits as one undo step; any other file gets an unsaved
54
+ * draft, since nothing is written to disk on a server's word alone, unless the edit also moves a file: then it
55
+ * is one change, saved and moved through the machine. A file the edit creates is made through the machine.
57
56
  */
58
57
  export async function applyWorkspaceEdit(edit, host) {
59
58
  const steps = stepsOf(edit);
@@ -141,7 +140,7 @@ async function plan(steps, host) {
141
140
  created.text = applyTextEdits(created.text, step.edits);
142
141
  }
143
142
  catch (error) {
144
- return error instanceof Error ? error.message : String(error);
143
+ return messageOf(error);
145
144
  }
146
145
  continue;
147
146
  }
@@ -165,7 +164,7 @@ async function plan(steps, host) {
165
164
  after = applyTextEdits(held.text, step.edits);
166
165
  }
167
166
  catch (error) {
168
- return error instanceof Error ? error.message : String(error);
167
+ return messageOf(error);
169
168
  }
170
169
  planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
171
170
  texts.set(source, { text: after, disk: held.disk });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "type": "module",
5
5
  "description": "Language-enabled editors, feature coordination and React popups over injected language services.",
6
6
  "license": "FSL-1.1-MIT",
@@ -57,9 +57,9 @@
57
57
  "test": "bun test --conditions=source"
58
58
  },
59
59
  "dependencies": {
60
- "@adecore/editor": "0.21.0",
61
- "@adecore/lsp": "0.21.0",
62
- "@adecore/merge": "0.21.0",
60
+ "@adecore/editor": "0.21.2",
61
+ "@adecore/lsp": "0.21.2",
62
+ "@adecore/merge": "0.21.2",
63
63
  "lucide-react": "^1.47.0",
64
64
  "react-markdown": "^10.1.0",
65
65
  "remark-breaks": "^4.0.0",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.15"
68
68
  },
69
69
  "peerDependencies": {
70
- "@adecore/ui": "^0.21.0",
70
+ "@adecore/ui": "^0.21.2",
71
71
  "i18next": "^26.4.2",
72
72
  "react": "^19.3.0",
73
73
  "react-dom": "^19.3.0",
@@ -14,9 +14,8 @@ export interface AnchoredPopupProps {
14
14
  }
15
15
 
16
16
  /*
17
- * A card next to a character of the editor, in a layer of the page itself: a canvas puts a scale on its
18
- * nodes, and a card inside one would be drawn at that scale and cut off by the node's edge. It is
19
- * placed from the character's screen rectangle, so it is right at any zoom, and it never leaves the window.
17
+ * A card next to a character of the editor, portaled to the page: inside a scaled canvas node it would be
18
+ * drawn at that scale and clipped by the node. Placed from the character's screen rectangle, so it holds at any zoom.
20
19
  */
21
20
  export function AnchoredPopup({ rect, className, children, placement, onPointerEnter, onPointerLeave }: AnchoredPopupProps) {
22
21
  const element = useRef<HTMLDivElement>(null);