@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
@@ -1,8 +1,8 @@
1
1
  import type { Diagnostic } from '@adecore/lsp';
2
- import type { EditorContentChange, EditorPosition } from '@adecore/editor';
2
+ import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { isShortcut } from './shortcut-keys.ts';
5
- import { comparePositions, markerOf, neighborProblem, problemsAt, shiftRange, type Problem } from './diagnostics-model.ts';
5
+ import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, shiftRange, type Problem } from './diagnostics-model.ts';
6
6
 
7
7
  export interface ProblemCounts {
8
8
  readonly error: number;
@@ -54,8 +54,7 @@ export class DiagnosticsFeature {
54
54
  }
55
55
 
56
56
  counts(): ProblemCounts {
57
- const count = (severity: number): number => this.list.filter((problem) => (problem.diagnostic.severity ?? 1) === severity).length;
58
- return { error: count(1), warning: count(2), info: count(3) };
57
+ return problemCountsOf(this.list.map((problem) => problem.diagnostic));
59
58
  }
60
59
 
61
60
  at(position: EditorPosition): Problem[] {
@@ -64,27 +63,23 @@ export class DiagnosticsFeature {
64
63
 
65
64
  /* Puts the caret on the next problem, or the one before; false when there is none. */
66
65
  step(direction: 1 | -1): boolean {
67
- const { editor } = this.language;
68
- const target = neighborProblem(this.list, editor.getCaret(), direction);
66
+ const caret = this.language.editor.getCaret();
67
+ const target = neighborProblem(this.list, caret, direction);
69
68
  if (target === null) {
70
69
  return false;
71
70
  }
72
- editor.setCaret(target.diagnostic.range.start, comparePositions(target.diagnostic.range.start, editor.getCaret()) > 0 ? 'centerDown' : 'centerUp');
73
- editor.focus();
74
- this.language.hover.showProblemsAt(editor.getCaret());
71
+ const { start } = target.diagnostic.range;
72
+ this.reveal(start, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
75
73
  return true;
76
74
  }
77
75
 
78
76
  /* Puts the caret on the first problem of the file. */
79
77
  goToFirst(): boolean {
80
- const { editor } = this.language;
81
78
  const target = neighborProblem(this.list, { line: -1, character: 0 }, 1);
82
79
  if (target === null) {
83
80
  return false;
84
81
  }
85
- editor.setCaret(target.diagnostic.range.start, 'center');
86
- editor.focus();
87
- this.language.hover.showProblemsAt(editor.getCaret());
82
+ this.reveal(target.diagnostic.range.start, 'center');
88
83
  return true;
89
84
  }
90
85
 
@@ -95,6 +90,13 @@ export class DiagnosticsFeature {
95
90
  };
96
91
  }
97
92
 
93
+ private reveal(position: EditorPosition, reveal: EditorReveal): void {
94
+ const { editor } = this.language;
95
+ editor.setCaret(position, reveal);
96
+ editor.focus();
97
+ this.language.hover.showProblemsAt(editor.getCaret());
98
+ }
99
+
98
100
  private shift(changes: readonly EditorContentChange[]): void {
99
101
  for (const [server, diagnostics] of this.reports) {
100
102
  this.reports.set(
@@ -1,5 +1,4 @@
1
1
  import { KEYMAP, type Keymap } from '@adecore/editor/keymap';
2
- import { shortcutsOf } from './shortcut-keys.ts';
3
2
  import type { Editor, EditorPosition } from '@adecore/editor';
4
3
  import type { Location } from '@adecore/lsp';
5
4
  import { CodeActionsFeature } from './code-actions.ts';
@@ -17,6 +16,7 @@ import { PeekFeature } from './peek.ts';
17
16
  import { PickFeature } from './pick.ts';
18
17
  import { SymbolPickerFeature } from './symbol-picker.ts';
19
18
  import { RenameFeature } from './rename.ts';
19
+ import { shortcutsOf } from './shortcut-keys.ts';
20
20
  import { createPopupStore } from './popups.ts';
21
21
  import { realTimers, type Timers } from './timers.ts';
22
22
  import { InlayHintsFeature } from './inlay-hints.ts';
@@ -95,16 +95,6 @@ export class EditorLanguage {
95
95
  this.onDispose(editor.onCaret((position) => this.placeOpenedCaret(position)));
96
96
  }
97
97
 
98
- /* A file opened from another one lands on the line; this puts the caret on the column the jump meant. */
99
- private placeOpenedCaret(position: EditorPosition): void {
100
- const column = this.project.takeCaret(this.uri, position.line);
101
- if (column !== null && column.character !== position.character) {
102
- this.editor.setCaret(column);
103
- return;
104
- }
105
- this.project.noteCaret({ uri: this.uri, position });
106
- }
107
-
108
98
  get uri(): string {
109
99
  return this.document.uri;
110
100
  }
@@ -186,4 +176,14 @@ export class EditorLanguage {
186
176
  }
187
177
  this.document.release();
188
178
  }
179
+
180
+ /* A file opened from another one lands on the line; this puts the caret on the column the jump meant. */
181
+ private placeOpenedCaret(position: EditorPosition): void {
182
+ const column = this.project.takeCaret(this.uri, position.line);
183
+ if (column !== null && column.character !== position.character) {
184
+ this.editor.setCaret(column);
185
+ return;
186
+ }
187
+ this.project.noteCaret({ uri: this.uri, position });
188
+ }
189
189
  }
@@ -0,0 +1,3 @@
1
+ export function messageOf(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
package/src/folding.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { EditorFoldHints, EditorFoldRangeHint, EditorFoldSymbol } from '@adecore/editor';
2
- import type { DocumentSymbol, DocumentSymbolResult, FoldingRange, SymbolInformation } from '@adecore/lsp';
2
+ import type { DocumentSymbolResult, FoldingRange } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { Refresher } from './refresher.ts';
5
+ import { symbolSpansOf } from './symbols.ts';
5
6
  import { realTimers, type Timers } from './timers.ts';
6
7
 
7
8
  const METHOD = 'textDocument/foldingRange';
@@ -22,33 +23,12 @@ const BODIES: Readonly<Record<number, EditorFoldSymbol['body']>> = {
22
23
  23: 'class'
23
24
  };
24
25
 
25
- function walk(symbols: readonly DocumentSymbol[], found: EditorFoldSymbol[]): void {
26
- for (const symbol of symbols) {
27
- const body = BODIES[symbol.kind];
28
- if (body !== undefined && symbol.range.end.line > symbol.range.start.line) {
29
- found.push({ range: symbol.range, body });
30
- }
31
- walk(symbol.children ?? [], found);
32
- }
33
- }
34
-
35
26
  /* The symbols that have a body of more than one line, as what the editor folds them by. */
36
27
  export function foldSymbolsOf(result: DocumentSymbolResult): EditorFoldSymbol[] {
37
- const found: EditorFoldSymbol[] = [];
38
- if (result === null || result.length === 0) {
39
- return found;
40
- }
41
- if ('range' in result[0]!) {
42
- walk(result as DocumentSymbol[], found);
43
- return found;
44
- }
45
- for (const symbol of result as SymbolInformation[]) {
46
- const body = BODIES[symbol.kind];
47
- if (body !== undefined && symbol.location.range.end.line > symbol.location.range.start.line) {
48
- found.push({ range: symbol.location.range, body });
49
- }
50
- }
51
- return found;
28
+ return symbolSpansOf(result).flatMap(({ kind, range }) => {
29
+ const body = BODIES[kind];
30
+ return body !== undefined && range.end.line > range.start.line ? [{ range, body }] : [];
31
+ });
52
32
  }
53
33
 
54
34
  /* The ranges a server folds that span more than one line, with where a range starts and ends within its lines when it says. */
package/src/history.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EditorLanguage } from './editor-language.ts';
2
+ import type { Place } from './navigation-history.ts';
2
3
  import { isShortcut } from './shortcut-keys.ts';
3
4
 
4
5
  /* Back, Forward and the list of recent locations over the history the whole project shares. */
@@ -27,21 +28,11 @@ export class HistoryFeature {
27
28
 
28
29
  /* Returns to where the caret was before the last jump; false when there is no such place. */
29
30
  back(): boolean {
30
- const place = this.language.project.history.back(this.language.place);
31
- if (place === null) {
32
- return false;
33
- }
34
- this.language.visit(place);
35
- return true;
31
+ return this.visit(this.language.project.history.back(this.language.place));
36
32
  }
37
33
 
38
34
  forward(): boolean {
39
- const place = this.language.project.history.forward(this.language.place);
40
- if (place === null) {
41
- return false;
42
- }
43
- this.language.visit(place);
44
- return true;
35
+ return this.visit(this.language.project.history.forward(this.language.place));
45
36
  }
46
37
 
47
38
  /* Lists the places of the history under the caret; choosing one jumps there like any other jump. */
@@ -53,4 +44,12 @@ export class HistoryFeature {
53
44
  this.language.project.i18n.t('editor:language.history.recent')
54
45
  );
55
46
  }
47
+
48
+ private visit(place: Place | null): boolean {
49
+ if (place === null) {
50
+ return false;
51
+ }
52
+ this.language.visit(place);
53
+ return true;
54
+ }
56
55
  }
@@ -52,10 +52,12 @@ export function hoverTextOf(hover: Hover): HoverText {
52
52
  if (typeof contents === 'string' || 'language' in contents) {
53
53
  return markedStringText(contents);
54
54
  }
55
- // Plain text is text, and not markdown that happens to have underscores in it.
56
- return contents.kind === 'plaintext'
57
- ? { signatures: [], markdown: contents.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1').trim() }
58
- : splitSignatures(contents.value);
55
+ return contents.kind === 'plaintext' ? { signatures: [], markdown: escapeMarkdown(contents.value).trim() } : splitSignatures(contents.value);
56
+ }
57
+
58
+ /* Plain text as markdown that reads the same, its markdown characters escaped. */
59
+ export function escapeMarkdown(text: string): string {
60
+ return text.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1');
59
61
  }
60
62
 
61
63
  export function isEmptyHover(text: HoverText): boolean {
package/src/hover.ts CHANGED
@@ -2,6 +2,7 @@ import type { EditorHover, EditorPosition, EditorRange } from '@adecore/editor';
2
2
  import type { EditorLanguage } from './editor-language.ts';
3
3
  import { rangeHolds, type Problem } from './diagnostics-model.ts';
4
4
  import { hoverTextOf, isEmptyHover, locationsOf } from './hover-content.ts';
5
+ import { lineTextOf } from './line-text.ts';
5
6
  import { wordRangeAt } from './rename-model.ts';
6
7
  import type { HoverInfo } from './popups.ts';
7
8
  import { isShortcut } from './shortcut-keys.ts';
@@ -21,11 +22,11 @@ const TOAST_ID = 'language-hover';
21
22
  export class HoverFeature {
22
23
  private readonly language: EditorLanguage;
23
24
  private readonly timers: Timers;
24
- private showTimer: unknown;
25
- private countTimer: unknown;
26
25
  /* The counts asked for since the text last changed, by the symbol's position, so moving back to a name does not ask again. */
27
26
  private readonly counts = new Map<string, number>();
27
+ private showTimer: unknown;
28
28
  private hideTimer: unknown;
29
+ private countTimer: unknown;
29
30
  private inCard = false;
30
31
  /* Counts every request, so an answer that arrives after the pointer moved on is dropped. */
31
32
  private request = 0;
@@ -181,12 +182,15 @@ export class HoverFeature {
181
182
  service.hover(uri, position, { signal }).catch(() => null),
182
183
  service.supports('textDocument/definition', uri) ? service.definition(uri, position, { signal }).catch(() => null) : null
183
184
  ]);
184
- const text = hover === null ? null : hoverTextOf(hover);
185
- if (hover === null || text === null || isEmptyHover(text)) {
185
+ if (hover === null) {
186
+ return { info: null, range: null };
187
+ }
188
+ const text = hoverTextOf(hover);
189
+ if (isEmptyHover(text)) {
186
190
  return { info: null, range: null };
187
191
  }
188
192
  const { editor } = this.language;
189
- const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
193
+ const line = lineTextOf(editor, position.line);
190
194
  const word = wordRangeAt(line, position);
191
195
  return {
192
196
  info: {
@@ -271,10 +275,7 @@ export class HoverFeature {
271
275
 
272
276
  /* Where the word at a position starts, which names the symbol for the cache. */
273
277
  wordStart(position: EditorPosition): number {
274
- const line = this.language.editor.textInRange({
275
- start: { line: position.line, character: 0 },
276
- end: { line: position.line, character: Number.MAX_SAFE_INTEGER }
277
- });
278
+ const line = lineTextOf(this.language.editor, position.line);
278
279
  return wordRangeAt(line, position)?.start.character ?? position.character;
279
280
  }
280
281
  }
@@ -1,6 +1,7 @@
1
1
  import type { EditorInlayHint, EditorPosition, EditorRange } from '@adecore/editor';
2
2
  import type { InlayHint } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
+ import { lineTextOf } from './line-text.ts';
4
5
  import { Refresher } from './refresher.ts';
5
6
  import { realTimers, type Timers } from './timers.ts';
6
7
 
@@ -34,9 +35,8 @@ export function covers(asked: EditorRange, visible: EditorRange): boolean {
34
35
  }
35
36
 
36
37
  /*
37
- * Types and parameter names the servers infer, drawn in the text as soft pills. They are asked for the
38
- * lines in view and a margin around them after a pause in typing or scrolling; until the answer comes the
39
- * last ones stay where their text went, so nothing blinks while typing. Scrolling asks again only once the
38
+ * Types and parameter names the servers infer, asked for the lines in view and a margin around them. The
39
+ * last ones stay until the answer comes, so nothing blinks while typing; scrolling asks again only once the
40
40
  * view leaves the lines that were asked for.
41
41
  */
42
42
  export class InlayHintsFeature {
@@ -54,7 +54,7 @@ export class InlayHintsFeature {
54
54
  }
55
55
  const range = withMargin(editor.getVisibleRange(), editor.positionAt(Number.MAX_SAFE_INTEGER), (line) => ({
56
56
  line,
57
- character: editor.textInRange({ start: { line, character: 0 }, end: { line, character: Number.MAX_SAFE_INTEGER } }).length
57
+ character: lineTextOf(editor, line).length
58
58
  }));
59
59
  this.asked = range;
60
60
  const hints = await service.inlayHints(uri, range, { signal });
@@ -1,4 +1,4 @@
1
- /* The highlighter ids `fs.read` answers with, by the id LSP names the same language. */
1
+ /* Highlighter ids mapped to the LSP id of the same language. */
2
2
  const LSP_IDS: Record<string, string> = {
3
3
  typescript: 'typescript',
4
4
  tsx: 'typescriptreact',
@@ -0,0 +1,10 @@
1
+ import type { Editor, EditorPosition } from '@adecore/editor';
2
+
3
+ export function lineTextOf(editor: Editor, line: number): string {
4
+ return editor.textInRange({ start: { line, character: 0 }, end: { line, character: Number.MAX_SAFE_INTEGER } });
5
+ }
6
+
7
+ /* The text of a position's line up to the position. */
8
+ export function textBeforeOf(editor: Editor, position: EditorPosition): string {
9
+ return editor.textInRange({ start: { line: position.line, character: 0 }, end: position });
10
+ }
package/src/navigation.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { StaleResultError, fileUriToPath, type Location, type LocationLink, type NavigationResult, type Range } from '@adecore/lsp';
1
+ import { StaleResultError, fileUriToPath, type LanguageService, type Location, type LocationLink, type NavigationResult, type Range } from '@adecore/lsp';
2
2
  import type { EditorPosition } from '@adecore/editor';
3
3
  import { basenameOf } from './paths.ts';
4
4
  import type { EditorLanguage } from './editor-language.ts';
5
5
  import { rangeHolds } from './diagnostics-model.ts';
6
6
  import { placesOfName } from './symbol-links.ts';
7
7
  import { isShortcut } from './shortcut-keys.ts';
8
+ import { messageOf } from './error-message.ts';
8
9
 
9
10
  export type NavigationKind = 'definition' | 'declaration' | 'typeDefinition' | 'implementation';
10
11
 
@@ -15,6 +16,13 @@ const METHODS: Record<NavigationKind, string> = {
15
16
  implementation: 'textDocument/implementation'
16
17
  };
17
18
 
19
+ const REQUESTS: Record<NavigationKind, (service: LanguageService, uri: string, position: EditorPosition) => Promise<NavigationResult>> = {
20
+ definition: (service, uri, position) => service.definition(uri, position),
21
+ declaration: (service, uri, position) => service.declaration(uri, position),
22
+ typeDefinition: (service, uri, position) => service.typeDefinition(uri, position),
23
+ implementation: (service, uri, position) => service.implementation(uri, position)
24
+ };
25
+
18
26
  const SHORTCUT_KINDS: readonly [NavigationKind, 'goToDefinition' | 'goToTypeDefinition' | 'goToImplementation'][] = [
19
27
  ['definition', 'goToDefinition'],
20
28
  ['typeDefinition', 'goToTypeDefinition'],
@@ -96,17 +104,10 @@ export class NavigationFeature {
96
104
  }
97
105
  let result: NavigationResult;
98
106
  try {
99
- const { service } = project;
100
- result = await (kind === 'definition'
101
- ? service.definition(uri, position)
102
- : kind === 'declaration'
103
- ? service.declaration(uri, position)
104
- : kind === 'typeDefinition'
105
- ? service.typeDefinition(uri, position)
106
- : service.implementation(uri, position));
107
+ result = await REQUESTS[kind](project.service, uri, position);
107
108
  } catch (error) {
108
109
  if (!(error instanceof StaleResultError)) {
109
- this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
110
+ this.tell(this.say('failed', { message: messageOf(error) }));
110
111
  }
111
112
  return;
112
113
  }
package/src/paths.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /* A path inside `folder` relative to it; any other path as it is. */
2
+ export function relativeTo(path: string, folder: string): string {
3
+ return path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
4
+ }
5
+
1
6
  export function basenameOf(path: string): string {
2
7
  return path.replaceAll('\\', '/').split('/').at(-1) ?? path;
3
8
  }
package/src/peek.ts CHANGED
@@ -5,6 +5,7 @@ import { uniquePlaces } from './navigation.ts';
5
5
  import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf, type PeekFile } from './peek-model.ts';
6
6
  import type { PeekView } from './popups.ts';
7
7
  import { isShortcut } from './shortcut-keys.ts';
8
+ import { messageOf } from './error-message.ts';
8
9
 
9
10
  const TOAST_ID = 'language-peek';
10
11
  /* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
@@ -79,7 +80,7 @@ export class PeekFeature {
79
80
  found = await this.fetch(kind, position);
80
81
  } catch (error) {
81
82
  if (!(error instanceof StaleResultError)) {
82
- this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
83
+ this.tell(this.say('failed', { message: messageOf(error) }));
83
84
  }
84
85
  return;
85
86
  }
@@ -87,15 +88,15 @@ export class PeekFeature {
87
88
  return;
88
89
  }
89
90
  const { targets } = found;
90
- const unique = found.locations.filter(
91
- (location, index) =>
92
- found.locations.findIndex(
93
- (other) =>
94
- other.uri === location.uri &&
95
- other.range.start.line === location.range.start.line &&
96
- other.range.start.character === location.range.start.character
97
- ) === index
98
- );
91
+ const seen = new Set<string>();
92
+ const unique = found.locations.filter((location) => {
93
+ const key = placeKey(location);
94
+ if (seen.has(key)) {
95
+ return false;
96
+ }
97
+ seen.add(key);
98
+ return true;
99
+ });
99
100
  if (unique.length === 0) {
100
101
  this.tell(this.say(KINDS[kind].none));
101
102
  return;
@@ -5,6 +5,7 @@ import { NavigationHistory, type Place } from './navigation-history.ts';
5
5
  import { ProjectProblems } from './project-problems.ts';
6
6
  import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
7
7
  import type { LanguageHost } from './host-types.ts';
8
+ import { relativeTo } from './paths.ts';
8
9
 
9
10
  export interface LanguageDocumentHandle {
10
11
  readonly uri: string;
@@ -13,16 +14,16 @@ export interface LanguageDocumentHandle {
13
14
  release(): void;
14
15
  }
15
16
 
17
+ /* Sends one editor's edits to the document, holding them until the document is open. */
16
18
  class DocumentSync {
17
19
  readonly ready: Promise<void>;
20
+ readonly editor: Editor;
21
+ private readonly service: LanguageService;
22
+ private readonly uri: string;
23
+ private readonly stop: () => void;
18
24
  private pending: ContentChange[] = [];
19
25
  private opened = false;
20
26
  private disposed = false;
21
- private readonly stop: () => void;
22
-
23
- private readonly service: LanguageService;
24
- private readonly uri: string;
25
- readonly editor: Editor;
26
27
 
27
28
  constructor(service: LanguageService, uri: string, languageId: string, editor: Editor) {
28
29
  this.service = service;
@@ -39,6 +40,16 @@ class DocumentSync {
39
40
  this.ready.catch(() => undefined);
40
41
  }
41
42
 
43
+ get text(): string {
44
+ return this.editor.getText();
45
+ }
46
+
47
+ dispose(): void {
48
+ this.disposed = true;
49
+ this.pending = [];
50
+ this.stop();
51
+ }
52
+
42
53
  private changed(changes: readonly ContentChange[]): void {
43
54
  this.pending.push(...changes);
44
55
  if (this.opened) {
@@ -54,16 +65,6 @@ class DocumentSync {
54
65
  this.pending = [];
55
66
  void this.service.changeDocument(this.uri, changes).catch(() => undefined);
56
67
  }
57
-
58
- dispose(): void {
59
- this.disposed = true;
60
- this.pending = [];
61
- this.stop();
62
- }
63
-
64
- get text(): string {
65
- return this.editor.getText();
66
- }
67
68
  }
68
69
 
69
70
  interface Holder {
@@ -103,7 +104,7 @@ export class ProjectLanguage {
103
104
  return this.host.pathOfUri(uri);
104
105
  }
105
106
  const path = fileUriToPath(uri);
106
- return path !== null && this.folder !== '' && path.startsWith(`${this.folder}/`) ? path.slice(this.folder.length + 1) : path;
107
+ return path !== null && this.folder !== '' ? relativeTo(path, this.folder) : path;
107
108
  }
108
109
 
109
110
  /* Makes an edit of a language server: an open document takes it as one undo step and any other file gets an unsaved draft. */
@@ -1,6 +1,5 @@
1
- import type { Diagnostic } from '@adecore/lsp';
2
- import type { LanguageService } from '@adecore/lsp';
3
- import { comparePositions, severityOf } from './diagnostics-model.ts';
1
+ import type { Diagnostic, LanguageService } from '@adecore/lsp';
2
+ import { comparePositions, problemCountsOf, severityOf } from './diagnostics-model.ts';
4
3
  import type { ProblemCounts } from './diagnostics.ts';
5
4
 
6
5
  /* One problem of a file and the server that reported it. */
@@ -81,7 +80,5 @@ export class ProjectProblems {
81
80
  }
82
81
 
83
82
  export function countsOf(files: readonly ProblemFile[]): ProblemCounts {
84
- const rows = files.flatMap((file) => file.rows);
85
- const count = (severity: number): number => rows.filter((row) => (row.diagnostic.severity ?? 1) === severity).length;
86
- return { error: count(1), warning: count(2), info: count(3) };
83
+ return problemCountsOf(files.flatMap((file) => file.rows.map((row) => row.diagnostic)));
87
84
  }
package/src/refresher.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { StaleResultError } from '@adecore/lsp';
1
+ import { ErrorCodes, StaleResultError } from '@adecore/lsp';
2
2
  import { realTimers, type Timers } from './timers.ts';
3
3
 
4
4
  /*
5
- * Asks again for something a document's text decides, such as its colors or its hints: after a pause
6
- * in typing, when a server says what it offers changed, and never twice at once. A newer ask aborts the
7
- * one before it, and an answer for a text that has moved on is dropped, since another ask is on its way.
5
+ * Asks again for something a document's text decides, such as its colors or hints, never twice at once.
6
+ * A newer ask aborts the one before it, and an answer for a text that moved on is dropped.
8
7
  */
9
8
  export class Refresher {
10
9
  private timer: unknown;
@@ -49,8 +48,15 @@ export class Refresher {
49
48
  }
50
49
  }
51
50
 
52
- /* The failures of a server that is not up, does not offer the feature or was asked to stop, which are not worth a line in the console. */
51
+ const QUIET_CODES: ReadonlySet<number> = new Set([
52
+ ErrorCodes.ServerNotInitialized,
53
+ ErrorCodes.MethodNotFound,
54
+ ErrorCodes.RequestCancelled,
55
+ ErrorCodes.ContentModified
56
+ ]);
57
+
58
+ /* A server that is not up, does not offer the feature or was asked to stop is not worth a line in the console. */
53
59
  function isQuiet(error: unknown): boolean {
54
60
  const code = (error as { code?: number } | null)?.code;
55
- return code === -32002 || code === -32601 || code === -32800 || code === -32801;
61
+ return code !== undefined && QUIET_CODES.has(code);
56
62
  }
package/src/rename.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { StaleResultError, type Location, type PrepareRenameResult, type WorkspaceEdit } from '@adecore/lsp';
2
2
  import type { EditorRange } from '@adecore/editor';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
+ import { lineTextOf } from './line-text.ts';
4
5
  import type { RenameView } from './popups.ts';
5
6
  import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt, type RenameTarget } from './rename-model.ts';
6
7
  import { isShortcut } from './shortcut-keys.ts';
8
+ import { messageOf } from './error-message.ts';
7
9
 
8
10
  const METHOD = 'textDocument/rename';
9
11
  const TOAST_ID = 'language-rename';
10
- /* How many names are offered, and how many other lines and files are read to say how the symbol is used. */
12
+ /* How much is read to tell a name suggester how the symbol is used: its first places, other files and lines around it. */
11
13
  const SAMPLE_LOCATIONS = 8;
12
14
  const SAMPLE_OTHER_FILES = 3;
13
15
  const CONTEXT_LINES = 3;
@@ -27,10 +29,6 @@ interface Session {
27
29
  * Edits to other files land as unsaved drafts (`applyWorkspaceEdit`), so nothing is written to disk here.
28
30
  */
29
31
  export class RenameFeature {
30
- private tell(kind: 'success' | 'error', title: string): void {
31
- this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
32
- }
33
-
34
32
  private readonly language: EditorLanguage;
35
33
  private session: Session | null = null;
36
34
 
@@ -80,11 +78,7 @@ export class RenameFeature {
80
78
  const target = renameTargetOf(
81
79
  answer,
82
80
  (range) => editor.textInRange(range),
83
- () =>
84
- wordRangeAt(
85
- editor.textInRange({ start: { line: caret.line, character: 0 }, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } }),
86
- caret
87
- )
81
+ () => wordRangeAt(lineTextOf(editor, caret.line), caret)
88
82
  );
89
83
  if (target === null) {
90
84
  this.tell('error', this.say('nothing'));
@@ -135,7 +129,7 @@ export class RenameFeature {
135
129
  this.publish({ phase: 'preview', busy: false, name: next, files });
136
130
  } catch (error) {
137
131
  if (this.session === session) {
138
- this.fail(error instanceof StaleResultError ? this.say('stale') : error instanceof Error ? error.message : String(error));
132
+ this.fail(error instanceof StaleResultError ? this.say('stale') : messageOf(error));
139
133
  }
140
134
  }
141
135
  }
@@ -155,6 +149,13 @@ export class RenameFeature {
155
149
  this.finish();
156
150
  }
157
151
 
152
+ /* The name was changed, which answers the failure under it. */
153
+ edited(): void {
154
+ if (this.language.popups.getState().rename?.error != null) {
155
+ this.publish({ error: null });
156
+ }
157
+ }
158
+
158
159
  private async apply(session: Session): Promise<void> {
159
160
  const result = await this.language.project.applyWorkspaceEdit(session.edit!);
160
161
  if (this.session !== session) {
@@ -175,13 +176,6 @@ export class RenameFeature {
175
176
  }
176
177
  }
177
178
 
178
- /* The name was changed, which answers the failure under it. */
179
- edited(): void {
180
- if (this.language.popups.getState().rename?.error != null) {
181
- this.publish({ error: null });
182
- }
183
- }
184
-
185
179
  private finish(): void {
186
180
  this.session?.names.abort();
187
181
  this.session = null;
@@ -284,6 +278,10 @@ export class RenameFeature {
284
278
  });
285
279
  }
286
280
 
281
+ private tell(kind: 'success' | 'error', title: string): void {
282
+ this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
283
+ }
284
+
287
285
  private say(key: string, options?: Record<string, unknown>): string {
288
286
  return this.language.project.i18n.t(`editor:language.rename.${key}`, options);
289
287
  }