@adecore/editor-react 0.17.0-beta.1 → 0.17.0-beta.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 (53) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +35 -30
  3. package/dist/EditorView.d.ts +3 -2
  4. package/dist/EditorView.js +2 -2
  5. package/dist/LanguagePopups.d.ts +3 -1
  6. package/dist/LanguagePopups.js +3 -3
  7. package/dist/code-actions.d.ts +1 -0
  8. package/dist/code-actions.js +19 -22
  9. package/dist/code-authors.d.ts +2 -1
  10. package/dist/code-authors.js +2 -3
  11. package/dist/code-vision-model.d.ts +2 -1
  12. package/dist/code-vision-model.js +3 -4
  13. package/dist/code-vision.js +12 -10
  14. package/dist/completion.js +1 -2
  15. package/dist/editor-language.d.ts +1 -0
  16. package/dist/editor-language.js +22 -0
  17. package/dist/folding.js +3 -5
  18. package/dist/history.js +1 -2
  19. package/dist/host-types.d.ts +2 -0
  20. package/dist/hover.js +1 -2
  21. package/dist/inlay-hints.js +3 -5
  22. package/dist/navigation.d.ts +1 -0
  23. package/dist/navigation.js +8 -9
  24. package/dist/peek.d.ts +1 -0
  25. package/dist/peek.js +6 -7
  26. package/dist/project-language.d.ts +2 -0
  27. package/dist/project-language.js +3 -0
  28. package/dist/rename.d.ts +1 -0
  29. package/dist/rename.js +10 -11
  30. package/dist/semantic-tokens.js +3 -5
  31. package/dist/symbol-picker.js +2 -3
  32. package/dist/symbols.js +3 -5
  33. package/package.json +5 -5
  34. package/src/EditorView.tsx +5 -3
  35. package/src/LanguagePopups.tsx +8 -4
  36. package/src/code-actions.ts +20 -23
  37. package/src/code-authors.ts +3 -3
  38. package/src/code-vision-model.ts +4 -4
  39. package/src/code-vision.ts +12 -10
  40. package/src/completion.ts +1 -2
  41. package/src/editor-language.ts +26 -0
  42. package/src/folding.ts +3 -5
  43. package/src/history.ts +1 -2
  44. package/src/host-types.ts +3 -0
  45. package/src/hover.ts +1 -2
  46. package/src/inlay-hints.ts +3 -5
  47. package/src/navigation.ts +9 -10
  48. package/src/peek.ts +7 -8
  49. package/src/project-language.ts +3 -0
  50. package/src/rename.ts +11 -12
  51. package/src/semantic-tokens.ts +3 -5
  52. package/src/symbol-picker.ts +2 -3
  53. package/src/symbols.ts +3 -5
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError, fileUriToPath } from '@adecore/lsp';
3
2
  import { basenameOf } from './paths.js';
4
3
  import { rangeHolds } from './diagnostics-model.js';
@@ -16,9 +15,6 @@ const SHORTCUT_KINDS = [
16
15
  ['implementation', 'goToImplementation']
17
16
  ];
18
17
  const TOAST_ID = 'language-navigation';
19
- function say(key, options) {
20
- return i18next.t(`editor:language.navigation.${key}`, options);
21
- }
22
18
  /* The places of a result, each once: the two servers of a Vue file may both know the same one. `target` is the whole declaration when the server gave it. */
23
19
  export function uniquePlaces(result) {
24
20
  const seen = new Set();
@@ -80,7 +76,7 @@ export class NavigationFeature {
80
76
  async go(kind, position = this.language.editor.getCaret()) {
81
77
  const { project, uri, editor } = this.language;
82
78
  if (!this.supports(kind)) {
83
- this.tell(say('unavailable'));
79
+ this.tell(this.say('unavailable'));
84
80
  return;
85
81
  }
86
82
  let result;
@@ -96,13 +92,13 @@ export class NavigationFeature {
96
92
  }
97
93
  catch (error) {
98
94
  if (!(error instanceof StaleResultError)) {
99
- this.tell(say('failed', { message: error instanceof Error ? error.message : String(error) }));
95
+ this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
100
96
  }
101
97
  return;
102
98
  }
103
99
  const places = uniqueLocations(result);
104
100
  if (places.length === 0) {
105
- this.tell(say('none'));
101
+ this.tell(this.say('none'));
106
102
  }
107
103
  else if (kind === 'definition' && places.length === 1 && places[0].uri === uri && rangeHolds(places[0].range, position)) {
108
104
  // The name is its own definition, so what a person wants to see is where it is used.
@@ -112,7 +108,7 @@ export class NavigationFeature {
112
108
  this.language.goTo(places[0]);
113
109
  }
114
110
  else {
115
- this.language.locations(places, position, say(`titles.${kind}`));
111
+ this.language.locations(places, position, this.say(`titles.${kind}`));
116
112
  }
117
113
  editor.focus();
118
114
  }
@@ -136,7 +132,7 @@ export class NavigationFeature {
136
132
  this.language.goTo(places[0]);
137
133
  }
138
134
  else if (places.length > 1) {
139
- this.language.locations(places, anchor, say('titles.symbol'));
135
+ this.language.locations(places, anchor, this.say('titles.symbol'));
140
136
  }
141
137
  }
142
138
  catch {
@@ -146,6 +142,9 @@ export class NavigationFeature {
146
142
  tell(title) {
147
143
  this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
148
144
  }
145
+ say(key, options) {
146
+ return this.language.project.i18n.t(`editor:language.navigation.${key}`, options);
147
+ }
149
148
  }
150
149
  /* How a place reads in a list: the file and the line, with the folder it is in. */
151
150
  export function locationRow(location, stored) {
package/dist/peek.d.ts CHANGED
@@ -25,5 +25,6 @@ export declare class PeekFeature {
25
25
  private key;
26
26
  private step;
27
27
  private tell;
28
+ private say;
28
29
  }
29
30
  export {};
package/dist/peek.js CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError } from '@adecore/lsp';
3
2
  import { uniquePlaces } from './navigation.js';
4
3
  import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf } from './peek-model.js';
@@ -13,9 +12,6 @@ const KINDS = {
13
12
  function placeKey(location) {
14
13
  return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
15
14
  }
16
- function say(key, options) {
17
- return i18next.t(`editor:language.peek.${key}`, options);
18
- }
19
15
  /*
20
16
  * The references of the name at the caret, between the lines of the file under the line it is on. The
21
17
  * editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
@@ -62,7 +58,7 @@ export class PeekFeature {
62
58
  const { editor, project, uri } = this.language;
63
59
  const method = KINDS[kind].method;
64
60
  if (!project.service.supports(method, uri)) {
65
- this.tell(say(KINDS[kind].unavailable));
61
+ this.tell(this.say(KINDS[kind].unavailable));
66
62
  return;
67
63
  }
68
64
  const token = ++this.token;
@@ -72,7 +68,7 @@ export class PeekFeature {
72
68
  }
73
69
  catch (error) {
74
70
  if (!(error instanceof StaleResultError)) {
75
- this.tell(say('failed', { message: error instanceof Error ? error.message : String(error) }));
71
+ this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
76
72
  }
77
73
  return;
78
74
  }
@@ -84,7 +80,7 @@ export class PeekFeature {
84
80
  other.range.start.line === location.range.start.line &&
85
81
  other.range.start.character === location.range.start.character) === index);
86
82
  if (unique.length === 0) {
87
- this.tell(say(KINDS[kind].none));
83
+ this.tell(this.say(KINDS[kind].none));
88
84
  return;
89
85
  }
90
86
  const uris = [...new Set(unique.map((location) => location.uri))].slice(0, PEEK_READ_FILES);
@@ -213,4 +209,7 @@ export class PeekFeature {
213
209
  tell(title) {
214
210
  this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
215
211
  }
212
+ say(key, options) {
213
+ return this.language.project.i18n.t(`editor:language.peek.${key}`, options);
214
+ }
216
215
  }
@@ -1,3 +1,4 @@
1
+ import { type i18n } from 'i18next';
1
2
  import { type ApplyWorkspaceEditResult, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
2
3
  import type { Editor, EditorPosition } from '@adecore/editor';
3
4
  import { NavigationHistory, type Place } from './navigation-history.ts';
@@ -12,6 +13,7 @@ export interface LanguageDocumentHandle {
12
13
  export declare class ProjectLanguage {
13
14
  readonly service: LanguageService;
14
15
  readonly host: LanguageHost;
16
+ readonly i18n: i18n;
15
17
  readonly folder: string;
16
18
  readonly problems: ProjectProblems;
17
19
  readonly history: NavigationHistory;
@@ -1,3 +1,4 @@
1
+ import i18next, {} from 'i18next';
1
2
  import { fileUriToPath } from '@adecore/lsp';
2
3
  import { NavigationHistory } from './navigation-history.js';
3
4
  import { ProjectProblems } from './project-problems.js';
@@ -55,6 +56,7 @@ class DocumentSync {
55
56
  export class ProjectLanguage {
56
57
  service;
57
58
  host;
59
+ i18n;
58
60
  folder;
59
61
  problems;
60
62
  history = new NavigationHistory();
@@ -66,6 +68,7 @@ export class ProjectLanguage {
66
68
  constructor(service, host = {}) {
67
69
  this.service = service;
68
70
  this.host = host;
71
+ this.i18n = host.i18n ?? i18next;
69
72
  this.folder = host.folder ?? '';
70
73
  this.files = host.files ?? null;
71
74
  this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
package/dist/rename.d.ts CHANGED
@@ -17,4 +17,5 @@ export declare class RenameFeature {
17
17
  private suggest;
18
18
  private linesOf;
19
19
  private publish;
20
+ private say;
20
21
  }
package/dist/rename.js CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError } from '@adecore/lsp';
3
2
  import { occurrencesOf, renamePreviewOf, renameTargetOf, wordRangeAt } from './rename-model.js';
4
3
  import { isShortcut } from './shortcut-keys.js';
@@ -8,9 +7,6 @@ const TOAST_ID = 'language-rename';
8
7
  const SAMPLE_LOCATIONS = 8;
9
8
  const SAMPLE_OTHER_FILES = 3;
10
9
  const CONTEXT_LINES = 3;
11
- function say(key, options) {
12
- return i18next.t(`editor:language.rename.${key}`, options);
13
- }
14
10
  /*
15
11
  * Renaming a symbol. The input sits on the symbol itself, every place the name has lights up while the
16
12
  * new one is typed, Enter applies the server's edit at once and Shift+Enter lists what it would change first.
@@ -48,7 +44,7 @@ export class RenameFeature {
48
44
  async start() {
49
45
  const { editor, project, uri } = this.language;
50
46
  if (!project.service.supports(METHOD, uri)) {
51
- this.tell('error', say('unavailable'));
47
+ this.tell('error', this.say('unavailable'));
52
48
  return;
53
49
  }
54
50
  this.cancel();
@@ -60,13 +56,13 @@ export class RenameFeature {
60
56
  }
61
57
  catch (error) {
62
58
  if (!(error instanceof StaleResultError)) {
63
- this.tell('error', say('nothing'));
59
+ this.tell('error', this.say('nothing'));
64
60
  }
65
61
  return;
66
62
  }
67
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));
68
64
  if (target === null) {
69
- this.tell('error', say('nothing'));
65
+ this.tell('error', this.say('nothing'));
70
66
  return;
71
67
  }
72
68
  const session = { target, original: editor.textInRange(target.range), edit: null, names: new AbortController() };
@@ -93,7 +89,7 @@ export class RenameFeature {
93
89
  return;
94
90
  }
95
91
  if (edit === null) {
96
- this.fail(say('refused'));
92
+ this.fail(this.say('refused'));
97
93
  return;
98
94
  }
99
95
  session.edit = edit;
@@ -114,7 +110,7 @@ export class RenameFeature {
114
110
  }
115
111
  catch (error) {
116
112
  if (this.session === session) {
117
- this.fail(error instanceof StaleResultError ? say('stale') : error instanceof Error ? error.message : String(error));
113
+ this.fail(error instanceof StaleResultError ? this.say('stale') : error instanceof Error ? error.message : String(error));
118
114
  }
119
115
  }
120
116
  }
@@ -140,14 +136,14 @@ export class RenameFeature {
140
136
  this.finish();
141
137
  }
142
138
  else {
143
- this.fail(result.failureReason ?? say('refused'));
139
+ this.fail(result.failureReason ?? this.say('refused'));
144
140
  }
145
141
  }
146
142
  /* The input stays, with the reason under it, so the name can be changed and tried again. */
147
143
  fail(message) {
148
144
  if (this.session !== null) {
149
145
  this.session.edit = null;
150
- this.publish({ phase: 'input', busy: false, files: [], error: say('failed', { message }) });
146
+ this.publish({ phase: 'input', busy: false, files: [], error: this.say('failed', { message }) });
151
147
  }
152
148
  }
153
149
  /* The name was changed, which answers the failure under it. */
@@ -255,4 +251,7 @@ export class RenameFeature {
255
251
  }
256
252
  });
257
253
  }
254
+ say(key, options) {
255
+ return this.language.project.i18n.t(`editor:language.rename.${key}`, options);
256
+ }
258
257
  }
@@ -25,14 +25,12 @@ export class SemanticTokensFeature {
25
25
  }
26
26
  }, PAUSE_MS, timers);
27
27
  const edits = editor.onTextChange(() => this.refresher.later());
28
- const providers = project.service.onProvidersChanged((changed) => {
29
- if (changed === uri) {
30
- this.refresher.now();
31
- }
28
+ const providers = language.onProvidersChanged(() => {
29
+ this.refresher.now();
32
30
  });
33
31
  language.onDispose(() => {
34
32
  edits();
35
- providers.dispose();
33
+ providers();
36
34
  this.refresher.dispose();
37
35
  editor.setSemanticTokens(null);
38
36
  });
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError } from '@adecore/lsp';
3
2
  import { basenameOf } from './paths.js';
4
3
  import { entriesOf, workspaceEntriesOf } from './symbol-picker-model.js';
@@ -28,7 +27,7 @@ export class SymbolPickerFeature {
28
27
  async open() {
29
28
  const { project, uri, popups } = this.language;
30
29
  if (!project.service.supports('textDocument/documentSymbol', uri)) {
31
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.symbols.unavailable') });
30
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.symbols.unavailable') });
32
31
  return;
33
32
  }
34
33
  try {
@@ -40,7 +39,7 @@ export class SymbolPickerFeature {
40
39
  this.language.project.host.notify?.({
41
40
  id: TOAST_ID,
42
41
  kind: 'error',
43
- title: i18next.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
42
+ title: this.language.project.i18n.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
44
43
  });
45
44
  }
46
45
  }
package/dist/symbols.js CHANGED
@@ -75,14 +75,12 @@ export class SymbolsFeature {
75
75
  }
76
76
  }, PAUSE_MS, timers);
77
77
  const edits = editor.onTextChange(() => this.refresher.later());
78
- const providers = project.service.onProvidersChanged((changed) => {
79
- if (changed === uri) {
80
- this.refresher.now();
81
- }
78
+ const providers = language.onProvidersChanged(() => {
79
+ this.refresher.now();
82
80
  });
83
81
  language.onDispose(() => {
84
82
  edits();
85
- providers.dispose();
83
+ providers();
86
84
  this.refresher.dispose();
87
85
  editor.setBlocks(null);
88
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.17.0-beta.1",
3
+ "version": "0.17.0-beta.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.17.0-beta.1",
61
- "@adecore/lsp": "0.17.0-beta.1",
62
- "@adecore/merge": "0.17.0-beta.1",
60
+ "@adecore/editor": "0.17.0-beta.2",
61
+ "@adecore/lsp": "0.17.0-beta.2",
62
+ "@adecore/merge": "0.17.0-beta.2",
63
63
  "zustand": "^5.0.15",
64
64
  "lucide-react": "^1.47.0",
65
65
  "react-markdown": "^10.1.0",
@@ -67,7 +67,7 @@
67
67
  "remark-breaks": "^4.0.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "@adecore/ui": "^0.17.0-beta.1",
70
+ "@adecore/ui": "^0.17.0-beta.2",
71
71
  "react": "^19.3.0",
72
72
  "react-dom": "^19.3.0",
73
73
  "i18next": "^26.4.2",
@@ -1,5 +1,5 @@
1
1
  import { createHolder } from './holder.ts';
2
- import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type Ref } from 'react';
2
+ import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode, type Ref } from 'react';
3
3
  import type { Editor, EditorEngine, EditorOptions } from '@adecore/editor';
4
4
  import { EditorLanguage } from './editor-language.ts';
5
5
  import { LanguagePopups } from './LanguagePopups.tsx';
@@ -14,9 +14,11 @@ export interface EditorViewProps {
14
14
  className?: string;
15
15
  ref?: Ref<HTMLDivElement>;
16
16
  onMount?(editor: Editor, language: EditorLanguage | null): void | (() => void);
17
+ /* Rows of the app in the context menu, between the refactorings and the clipboard rows. */
18
+ contextMenuItems?(language: EditorLanguage): ReactNode;
17
19
  }
18
20
 
19
- export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount }: EditorViewProps) {
21
+ export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount, contextMenuItems }: EditorViewProps) {
20
22
  const element = useRef<HTMLDivElement>(null);
21
23
  const holder = useMemo(() => createHolder<EditorLanguage>(), []);
22
24
  const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
@@ -39,7 +41,7 @@ export function EditorView({ engine, options, project, uri, languageId, classNam
39
41
  return (
40
42
  <div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
41
43
  <div ref={element} style={{ position: 'absolute', inset: 0 }} />
42
- {language !== null && <LanguagePopups language={language} />}
44
+ {language !== null && <LanguagePopups language={language} contextMenuItems={contextMenuItems} />}
43
45
  </div>
44
46
  );
45
47
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useReducer } from 'react';
1
+ import { useEffect, useReducer, type ReactNode } from 'react';
2
2
  import { useStore } from 'zustand';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { AnchoredPopup } from './AnchoredPopup.tsx';
@@ -15,9 +15,9 @@ import { SignatureCard } from './SignatureCard.tsx';
15
15
  /*
16
16
  * Every card a language feature opens over the editor. Each feature keeps its own state and these
17
17
  * only draw it, placed by the screen position of the character it belongs to, which is read again
18
- * whenever the editor scrolls or resizes.
18
+ * whenever the editor scrolls or resizes. `contextMenuItems` adds rows of the app to the context menu.
19
19
  */
20
- export function LanguagePopups({ language }: { language: EditorLanguage }) {
20
+ export function LanguagePopups({ language, contextMenuItems }: { language: EditorLanguage; contextMenuItems?: (language: EditorLanguage) => ReactNode }) {
21
21
  const [, redraw] = useReducer((count: number) => count + 1, 0);
22
22
  const hover = useStore(language.popups, (state) => state.hover);
23
23
  const completion = useStore(language.popups, (state) => state.completion);
@@ -51,7 +51,11 @@ export function LanguagePopups({ language }: { language: EditorLanguage }) {
51
51
  )}
52
52
  {completion !== null && completionRect !== null && <CompletionPopup language={language} view={completion} rect={completionRect} />}
53
53
  {pick !== null && pickRect !== null && <PickPopup language={language} view={pick} rect={pickRect} />}
54
- {menu !== null && <EditorContextMenu language={language} view={menu} />}
54
+ {menu !== null && (
55
+ <EditorContextMenu language={language} view={menu}>
56
+ {contextMenuItems?.(language)}
57
+ </EditorContextMenu>
58
+ )}
55
59
  {symbols !== null && <SymbolPicker language={language} view={symbols} />}
56
60
  {peek !== null && <PeekPanel language={language} view={peek} />}
57
61
  {authors !== null && <CodeAuthorsCard language={language} view={authors} />}
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { ErrorCodes, LspError, StaleResultError, type CodeAction, type Command, type Diagnostic, type Position } from '@adecore/lsp';
3
2
  import type { EditorPosition, EditorRange } from '@adecore/editor';
4
3
  import {
@@ -49,10 +48,6 @@ interface AskOptions {
49
48
  readonly groups?: readonly ActionGroup[];
50
49
  }
51
50
 
52
- function say(key: string, options?: Record<string, unknown>): string {
53
- return i18next.t(`editor:language.actions.${key}`, options);
54
- }
55
-
56
51
  /*
57
52
  * What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
58
53
  * that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
@@ -72,12 +67,10 @@ export class CodeActionsFeature {
72
67
 
73
68
  constructor(language: EditorLanguage, timers: Timers = realTimers) {
74
69
  this.language = language;
75
- const { editor, project, uri } = language;
70
+ const { editor } = language;
76
71
  this.refresher = new Refresher((signal) => this.hint(signal), PAUSE_MS, timers);
77
- const providers = project.service.onProvidersChanged((changed) => {
78
- if (changed === uri) {
79
- this.refresher.later();
80
- }
72
+ const providers = language.onProvidersChanged(() => {
73
+ this.refresher.later();
81
74
  });
82
75
  const offs = [
83
76
  editor.onCaret(() => {
@@ -98,7 +91,7 @@ export class CodeActionsFeature {
98
91
  return false;
99
92
  }),
100
93
  language.diagnostics.onChange(() => this.refresher.later()),
101
- () => providers.dispose()
94
+ providers
102
95
  ];
103
96
  language.onDispose(() => {
104
97
  for (const off of offs) {
@@ -137,7 +130,7 @@ export class CodeActionsFeature {
137
130
  const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
138
131
  const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
139
132
  if (entry === undefined) {
140
- this.tell('success', say(entries === null ? 'unavailable' : 'organized'));
133
+ this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
141
134
  return;
142
135
  }
143
136
  await this.apply(entry);
@@ -146,13 +139,13 @@ export class CodeActionsFeature {
146
139
  async formatDocument(): Promise<void> {
147
140
  const { editor, project, uri } = this.language;
148
141
  if (!project.service.supports('textDocument/formatting', uri)) {
149
- this.tell('error', say('noFormatter'));
142
+ this.tell('error', this.say('noFormatter'));
150
143
  return;
151
144
  }
152
145
  try {
153
146
  const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
154
147
  if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
155
- this.tell('error', say('readOnly'));
148
+ this.tell('error', this.say('readOnly'));
156
149
  }
157
150
  } catch (error) {
158
151
  this.fail(error);
@@ -175,7 +168,7 @@ export class CodeActionsFeature {
175
168
 
176
169
  private async ask(options: AskOptions): Promise<void> {
177
170
  if (!this.supported) {
178
- this.tell('error', say('unavailable'));
171
+ this.tell('error', this.say('unavailable'));
179
172
  return;
180
173
  }
181
174
  // The press on a card or the gutter may have taken the focus, and the list answers to the editor's keys.
@@ -193,13 +186,13 @@ export class CodeActionsFeature {
193
186
  return;
194
187
  }
195
188
  if (entries.length === 0) {
196
- this.tell('success', say('none'));
189
+ this.tell('success', this.say('none'));
197
190
  return;
198
191
  }
199
192
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
200
193
  const groups: PickGroup[] = (options.groups ?? ACTION_GROUPS).flatMap((group): PickGroup[] => {
201
194
  const rows = entries.filter((entry) => entry.group === group).map((entry) => ({ id: entry.id, label: entry.action.title, detail: '' }));
202
- return rows.length === 0 ? [] : [{ title: say(`groups.${group}`), rows }];
195
+ return rows.length === 0 ? [] : [{ title: this.say(`groups.${group}`), rows }];
203
196
  });
204
197
  this.language.completion.close();
205
198
  this.language.pick.open({
@@ -278,7 +271,7 @@ export class CodeActionsFeature {
278
271
  if (signal.aborted || this.asking) {
279
272
  return;
280
273
  }
281
- editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: say('show') } : null);
274
+ editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: this.say('show') } : null);
282
275
  }
283
276
 
284
277
  private async resolve(entry: ActionEntry, signal?: AbortSignal): Promise<CodeAction> {
@@ -325,9 +318,9 @@ export class CodeActionsFeature {
325
318
  return null;
326
319
  }
327
320
  const notes = [
328
- ...(found.hiddenLines > 0 ? [say('moreLines', { count: found.hiddenLines })] : []),
329
- ...(found.otherFiles > 0 ? [say('otherFiles', { count: found.otherFiles })] : []),
330
- ...(found.moves > 0 ? [say('moves', { count: found.moves })] : [])
321
+ ...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
322
+ ...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
323
+ ...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
331
324
  ];
332
325
  return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
333
326
  }
@@ -353,7 +346,7 @@ export class CodeActionsFeature {
353
346
  if (action.edit !== undefined) {
354
347
  const result = await project.applyWorkspaceEdit(action.edit);
355
348
  if (!result.applied) {
356
- this.tell('error', say('failed', { message: result.failureReason ?? say('refused') }));
349
+ this.tell('error', this.say('failed', { message: result.failureReason ?? this.say('refused') }));
357
350
  return;
358
351
  }
359
352
  }
@@ -381,7 +374,11 @@ export class CodeActionsFeature {
381
374
 
382
375
  private fail(error: unknown): void {
383
376
  if (!(error instanceof StaleResultError)) {
384
- this.tell('error', say('failed', { message: error instanceof Error ? error.message : String(error) }));
377
+ this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
385
378
  }
386
379
  }
380
+
381
+ private say(key: string, options?: Record<string, unknown>): string {
382
+ return this.language.project.i18n.t(`editor:language.actions.${key}`, options);
383
+ }
387
384
  }
@@ -1,4 +1,4 @@
1
- import i18next from 'i18next';
1
+ import type { i18n } from 'i18next';
2
2
  import { diffLines, splitLines } from '@adecore/merge';
3
3
  import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
4
4
  import { formatNumber } from '@adecore/ui/format';
@@ -94,10 +94,10 @@ export function authorshipOf(commits: readonly GitBlameCommit[], mapped: Int32Ar
94
94
  }
95
95
 
96
96
  /* The row's words: the author with most lines, how many others wrote some, and a star when the range is edited since the commit. */
97
- export function authorsText(authorship: CodeAuthorship): string {
97
+ export function authorsText(authorship: CodeAuthorship, i18n: i18n): string {
98
98
  const main = authorship.authors[0];
99
99
  if (main === undefined) {
100
- return i18next.t('editor:language.codeVision.newCode');
100
+ return i18n.t('editor:language.codeVision.newCode');
101
101
  }
102
102
  const others = authorship.authors.length - 1;
103
103
  return `${main.name}${others > 0 ? ` +${formatNumber(others)}` : ''}${authorship.modified ? ' *' : ''}`;
@@ -1,6 +1,6 @@
1
1
  import type { DocumentSymbol, DocumentSymbolResult, Position, SymbolInformation } from '@adecore/lsp';
2
2
  import { formatNumber } from '@adecore/ui/format';
3
- import i18next from 'i18next';
3
+ import type { i18n } from 'i18next';
4
4
 
5
5
  /* More declarations than this in one file and the rows are left out, which is the file that is too big for them to be quiet. */
6
6
  export const MAX_DECLARATIONS = 3000;
@@ -223,8 +223,8 @@ export function declarationsOf(result: DocumentSymbolResult, source: Declaration
223
223
  }
224
224
 
225
225
  /* "No usages", "1 usage" and "N usages". */
226
- export function usagesText(count: number): string {
226
+ export function usagesText(count: number, i18n: i18n): string {
227
227
  return count === 0
228
- ? i18next.t('editor:language.codeVision.usagesNone')
229
- : i18next.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
228
+ ? i18n.t('editor:language.codeVision.usagesNone')
229
+ : i18n.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
230
230
  }
@@ -62,7 +62,7 @@ export class CodeVisionFeature {
62
62
  constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
63
63
  this.language = language;
64
64
  this.timers = timers;
65
- const { editor, project, uri } = language;
65
+ const { editor } = language;
66
66
  const offs = [
67
67
  onSymbols((result) => this.read(result)),
68
68
  editor.onTextChange(() => {
@@ -74,19 +74,17 @@ export class CodeVisionFeature {
74
74
  this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
75
75
  })
76
76
  ];
77
- const providers = project.service.onProvidersChanged((changed) => {
78
- if (changed === uri) {
79
- this.failed.clear();
80
- this.publish();
81
- this.plan();
82
- }
77
+ const providers = language.onProvidersChanged(() => {
78
+ this.failed.clear();
79
+ this.publish();
80
+ this.plan();
83
81
  });
84
82
  language.onDispose(() => {
85
83
  this.disposed = true;
86
84
  for (const off of offs) {
87
85
  off();
88
86
  }
89
- providers.dispose();
87
+ providers();
90
88
  this.timers.clear(this.scrollTimer);
91
89
  for (const controller of this.running.values()) {
92
90
  controller.abort();
@@ -209,7 +207,11 @@ export class CodeVisionFeature {
209
207
  const entries: EditorCodeVisionEntry[] = [];
210
208
  const known = this.counts.get(declaration.id);
211
209
  if (counted && declaration.usages && known !== undefined) {
212
- entries.push({ id: 'usages', text: usagesText(known.count), activate: () => void this.language.peek.open(declaration.position) });
210
+ entries.push({
211
+ id: 'usages',
212
+ text: usagesText(known.count, this.language.project.i18n),
213
+ activate: () => void this.language.peek.open(declaration.position)
214
+ });
213
215
  }
214
216
  if (mapped !== null && declaration.authors) {
215
217
  let authorship = this.authorships.get(declaration.id);
@@ -220,7 +222,7 @@ export class CodeVisionFeature {
220
222
  const shown = authorship;
221
223
  entries.push({
222
224
  id: 'authors',
223
- text: authorsText(shown),
225
+ text: authorsText(shown, this.language.project.i18n),
224
226
  ...(shown.authors.length > 0 ? { icon: shown.authors.length > 1 ? ('users' as const) : ('user' as const) } : {}),
225
227
  activate: (anchor) => this.showAuthors(declaration, shown, anchor)
226
228
  });
package/src/completion.ts CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError, type CompletionContext, type CompletionItem } from '@adecore/lsp';
3
2
  import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
4
3
  import { comparePositions, shiftPosition } from './diagnostics-model.ts';
@@ -399,7 +398,7 @@ export class CompletionFeature {
399
398
  const asked = this.explicit && !refresh;
400
399
  this.refilter();
401
400
  if (asked && !this.isOpen && !this.incomplete) {
402
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.completion.none') });
401
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
403
402
  } else if (asked && this.ranked.length === 1 && !this.incomplete) {
404
403
  // One answer to a list that was asked for is the one wanted.
405
404
  void this.accept(false, 0);