@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
package/LICENSE CHANGED
@@ -6,7 +6,7 @@ FSL-1.1-MIT
6
6
 
7
7
  ## Notice
8
8
 
9
- Copyright 2026 Bas Milius
9
+ Copyright 2026 - present Bas Milius
10
10
 
11
11
  ## Terms and Conditions
12
12
 
package/README.md CHANGED
@@ -1,55 +1,60 @@
1
1
  # @adecore/editor-react
2
2
 
3
- Read the [React editor handbook](https://adecore.dev/editor-react/handbook/) for the complete setup, concepts, host integration and testing guides.
3
+ [![npm](https://img.shields.io/npm/v/@adecore/editor-react)](https://www.npmjs.com/package/@adecore/editor-react)
4
+ [![Docs](https://img.shields.io/badge/docs-adecore.dev-blue)](https://adecore.dev/editor-react/)
4
5
 
5
- Language-enabled editors and React popups over an injected `LanguageService`. Completion and snippets, hover, signature help, diagnostics, selection ranges, semantic tokens, inlay hints, folding, navigation, rename, code actions, symbols, peek and code vision retain their current implementation. The package starts no process and imports no application checkout.
6
+ Language features for [`@adecore/editor`](https://adecore.dev/editor/) over a `LanguageService` of [`@adecore/lsp`](https://adecore.dev/lsp/): suggestions and snippets, hover, signature help, problems, navigation, peek, rename, code actions, symbols, semantic colors, inlay hints, folds and code vision, with the React cards that show them. It starts no language server and touches no file; the app hands it a service and the file operations it allows.
6
7
 
7
- `ProjectLanguage` shares documents, navigation history and diagnostic counts across editors. `EditorLanguage` attaches the feature coordinators to one actual `Editor`. `EditorView` mounts an engine and renders `LanguagePopups`; use the coordinators directly when the host already owns mounting.
8
+ **[Documentation with live demos](https://adecore.dev/editor-react/)**
8
9
 
9
- ```tsx
10
- import { createSmartEditorEngine } from '@adecore/editor';
11
- import { resolveKeymap } from '@adecore/editor/keymap';
12
- import { EditorView, ProjectLanguage } from '@adecore/editor-react';
10
+ ## Install
13
11
 
14
- const keymap = resolveKeymap({ goToDefinition: { mac: 'Alt+Shift+D', other: 'Alt+Shift+D' } });
15
- const engine = createSmartEditorEngine({ tokenizer: async () => null, keymap, apple: false });
16
- const project = new ProjectLanguage(service, { keymap, apple: false, openPlace });
17
- const options = { text: 'const greeting = "Hello";', language: 'typescript', theme: 'github-light' };
18
-
19
- // Render inside a parent with a height; dispose the project when its owner closes it.
20
- <EditorView engine={engine} options={options} project={project} uri="file:///example.ts" languageId="typescript" className="h-72" />;
12
+ ```sh
13
+ bun add @adecore/editor-react
21
14
  ```
22
15
 
23
- Here `service` is the host's `LanguageService` and `openPlace` is its file-opening callback. Keep the engine, options, project and `onMount` callback stable across renders: changes remount the editor. `EditorView` disposes its editor and attachment on unmount; it does not dispose the supplied project or service. Dispose `ProjectLanguage` after its editor attachments, then let the host shut down its service. Editors sharing a URI must already share text through the host; the first editor sends document changes, and a surviving editor takes over when it closes.
24
-
25
- ## CSS and translations
16
+ `@adecore/ui`, React 19, `react-dom`, `i18next` and `react-i18next` are peer dependencies.
26
17
 
27
- Import these styles in order:
18
+ ## Set up
28
19
 
29
20
  ```css
30
21
  @import '@adecore/ui/theme.css';
31
22
  @import '@adecore/editor/editor.css';
32
23
  @import '@adecore/editor-react/editor-react.css';
33
- @source "../node_modules/@adecore/ui/src";
34
- @source "../node_modules/@adecore/editor-react/src";
24
+ @source "../node_modules/@adecore/editor-react/dist";
35
25
  ```
36
26
 
37
- The last two lines are Tailwind v4 source registrations, relative to the consumer stylesheet. React popups use Adecore UI components and Tailwind utilities. The editor DOM itself uses plain CSS and needs no Tailwind. The editor's positioned container must have a width and height. Define theme mode with `data-theme="light"` or `data-theme="dark"`, then override editor tokens after the imports if needed.
27
+ Add `@adecore/editor-react/locales/en.json` and `nl.json` to i18next as the `editor` namespace, then:
38
28
 
39
- Register `@adecore/editor-react/locales/en.json` and `/locales/nl.json` under the `editor` i18next namespace before mounting popups. The host also initializes the `@adecore/ui` translations. `EditorRenderingProvider` optionally accepts a theme and asynchronous code highlighter for hover and peek previews. Plain code is rendered when none is provided. Markdown supports GFM without raw HTML.
29
+ ```tsx
30
+ import { EditorView, ProjectLanguage } from '@adecore/editor-react';
40
31
 
41
- The working example in `examples/language-editor.tsx` owns an isolated i18next instance, mounts the real DOM engine and supplies `FakeLanguageService` completion and hover responses. Type text or press Ctrl+Space to request completion. It uses no filesystem, provider CLI or language-server process.
32
+ const project = new ProjectLanguage(service, { folder, keymap, openPlace, files, notify });
42
33
 
43
- ## Host operations
34
+ <EditorView engine={engine} options={options} project={project} uri={uri} languageId="typescript" className="h-96" />;
35
+ ```
44
36
 
45
- `LanguageHost` accepts the folder, platform, resolved keymap, path resolver, file-opening callback, server display names, notifications and optional rename suggestions. `ProjectFiles` supplies reads, unsaved staging, saves and renames. Open-file text edits are undoable; unopened files become drafts. Edits containing renames save their changes through the host before moving files. Create/delete operations remain unsupported and return a failure. Permission checks and drafts belong in these adapters.
37
+ ## Entry points
46
38
 
47
- Git authorship is supplied explicitly to `language.codeVision.setBlame`; the library runs no Git command. Attribution marks and remote cursors remain engine primitives. `AttributionCard` accepts display content and host action elements. `ChangeReview` renders an explicit selected/proposed diff; its `startLine` is one-based. `RowHost`, `LineActionHost` and `HighlightLayers` coordinate independent display owners. Acceptance, persistence, AI prompts, chat routing, conflict policy and rollback remain host operations.
39
+ | Import | What it holds |
40
+ |---|---|
41
+ | `@adecore/editor-react` | The components, `ProjectLanguage`, `EditorLanguage` and every model |
42
+ | `@adecore/editor-react/models` | The pure models, without React |
43
+ | `@adecore/editor-react/testing` | `FakeLanguageService` and `ManualTimers` |
44
+ | `@adecore/editor-react/editor-react.css` | The styles of the cards |
45
+ | `@adecore/editor-react/locales/*.json` | The words, in English and Dutch |
48
46
 
49
- `FindReplace` operates on an explicit editor. `EditorContextMenu` accepts host action children. `/models` exports the pure language, proposal, edit and view-state helpers; `/testing` exports `FakeLanguageService` and `ManualTimers`. Default imports load compiled JavaScript and declarations; workspace consumers can enable `source` in the bundler, TypeScript `customConditions` and `bun test --conditions=source`.
47
+ ## Documentation
50
48
 
51
- ## Current limits and validation
49
+ | Page | What it covers |
50
+ |---|---|
51
+ | [Getting started](https://adecore.dev/editor-react/getting-started) | Install, styles, words and a first editor |
52
+ | [Projects and documents](https://adecore.dev/editor-react/project) | `ProjectLanguage`, the host, shared documents and workspace edits |
53
+ | [EditorView](https://adecore.dev/editor-react/editor-view) | The component, `EditorLanguage` and the popups |
54
+ | [Views](https://adecore.dev/editor-react/completion) | A page per feature with a live demo, in the sidebar |
55
+ | [Models](https://adecore.dev/editor-react/models) | The pure functions behind the features |
56
+ | [Testing](https://adecore.dev/editor-react/testing) | `FakeLanguageService` and `ManualTimers` |
52
57
 
53
- This is the current implementation, with remaining feature work preserved. Language results depend on the injected service's supported providers. No bidirectional text or completed screen-reader validation is claimed. The engine retains its long-line coloring and row-list performance limits. File create/delete edits, AI workflows, language-server lifecycle and product shortcuts remain outside this package.
58
+ ## License
54
59
 
55
- Run `bun run build`, `bun run typecheck` and `bun run test` after workspace installation. Tests use editor fakes, memory services and manual timers. The package is private at version `0.0.0` and retains FSL-1.1-MIT. See the editor migration guide for source provenance and consumer cutover requirements.
60
+ FSL-1.1-MIT
@@ -1,4 +1,4 @@
1
- import { type Ref } from 'react';
1
+ import { type ReactNode, type Ref } from 'react';
2
2
  import type { Editor, EditorEngine, EditorOptions } from '@adecore/editor';
3
3
  import { EditorLanguage } from './editor-language.ts';
4
4
  import type { ProjectLanguage } from './project-language.ts';
@@ -11,5 +11,6 @@ export interface EditorViewProps {
11
11
  className?: string;
12
12
  ref?: Ref<HTMLDivElement>;
13
13
  onMount?(editor: Editor, language: EditorLanguage | null): void | (() => void);
14
+ contextMenuItems?(language: EditorLanguage): ReactNode;
14
15
  }
15
- export declare function EditorView({ engine, options, project, uri, languageId, className, ref, onMount }: EditorViewProps): import("react").JSX.Element;
16
+ export declare function EditorView({ engine, options, project, uri, languageId, className, ref, onMount, contextMenuItems }: EditorViewProps): import("react").JSX.Element;
@@ -3,7 +3,7 @@ import { createHolder } from './holder.js';
3
3
  import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
4
4
  import { EditorLanguage } from './editor-language.js';
5
5
  import { LanguagePopups } from './LanguagePopups.js';
6
- export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount }) {
6
+ export function EditorView({ engine, options, project, uri, languageId, className, ref, onMount, contextMenuItems }) {
7
7
  const element = useRef(null);
8
8
  const holder = useMemo(() => createHolder(), []);
9
9
  const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
@@ -23,5 +23,5 @@ export function EditorView({ engine, options, project, uri, languageId, classNam
23
23
  editor.dispose();
24
24
  };
25
25
  }, [engine, options, project, uri, languageId, holder, onMount]);
26
- return (_jsxs("div", { className: className, ref: ref, style: { position: 'relative', minHeight: 0 }, children: [_jsx("div", { ref: element, style: { position: 'absolute', inset: 0 } }), language !== null && _jsx(LanguagePopups, { language: language })] }));
26
+ return (_jsxs("div", { className: className, ref: ref, style: { position: 'relative', minHeight: 0 }, children: [_jsx("div", { ref: element, style: { position: 'absolute', inset: 0 } }), language !== null && _jsx(LanguagePopups, { language: language, contextMenuItems: contextMenuItems })] }));
27
27
  }
@@ -1,4 +1,6 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { EditorLanguage } from './editor-language.ts';
2
- export declare function LanguagePopups({ language }: {
3
+ export declare function LanguagePopups({ language, contextMenuItems }: {
3
4
  language: EditorLanguage;
5
+ contextMenuItems?: (language: EditorLanguage) => ReactNode;
4
6
  }): import("react").JSX.Element;
@@ -14,9 +14,9 @@ import { SignatureCard } from './SignatureCard.js';
14
14
  /*
15
15
  * Every card a language feature opens over the editor. Each feature keeps its own state and these
16
16
  * only draw it, placed by the screen position of the character it belongs to, which is read again
17
- * whenever the editor scrolls or resizes.
17
+ * whenever the editor scrolls or resizes. `contextMenuItems` adds rows of the app to the context menu.
18
18
  */
19
- export function LanguagePopups({ language }) {
19
+ export function LanguagePopups({ language, contextMenuItems }) {
20
20
  const [, redraw] = useReducer((count) => count + 1, 0);
21
21
  const hover = useStore(language.popups, (state) => state.hover);
22
22
  const completion = useStore(language.popups, (state) => state.completion);
@@ -34,5 +34,5 @@ export function LanguagePopups({ language }) {
34
34
  const pickRect = pick === null ? null : language.editor.rectAt(pick.anchor);
35
35
  const renameRect = rename === null ? null : language.editor.rectAt(rename.range.start);
36
36
  const renameEnd = rename === null ? null : language.editor.rectAt(rename.range.end);
37
- return (_jsxs(_Fragment, { children: [hover !== null && hoverRect !== null && (_jsx(AnchoredPopup, { rect: hoverRect, onPointerEnter: () => language.hover.holdCard(true), onPointerLeave: () => language.hover.holdCard(false), children: _jsx(HoverCard, { language: language, problems: hover.problems, info: hover.info, anchor: hover.anchor, position: hover.position }) })), signature !== null && signatureRect !== null && (_jsx(AnchoredPopup, { rect: signatureRect, placement: { prefer: 'above' }, children: _jsx(SignatureCard, { model: signature.model }) })), completion !== null && completionRect !== null && _jsx(CompletionPopup, { language: language, view: completion, rect: completionRect }), pick !== null && pickRect !== null && _jsx(PickPopup, { language: language, view: pick, rect: pickRect }), menu !== null && _jsx(EditorContextMenu, { language: language, view: menu }), symbols !== null && _jsx(SymbolPicker, { language: language, view: symbols }), peek !== null && _jsx(PeekPanel, { language: language, view: peek }), authors !== null && _jsx(CodeAuthorsCard, { language: language, view: authors }), rename !== null && renameRect !== null && _jsx(RenameCard, { language: language, view: rename, rect: renameRect, endRect: renameEnd })] }));
37
+ return (_jsxs(_Fragment, { children: [hover !== null && hoverRect !== null && (_jsx(AnchoredPopup, { rect: hoverRect, onPointerEnter: () => language.hover.holdCard(true), onPointerLeave: () => language.hover.holdCard(false), children: _jsx(HoverCard, { language: language, problems: hover.problems, info: hover.info, anchor: hover.anchor, position: hover.position }) })), signature !== null && signatureRect !== null && (_jsx(AnchoredPopup, { rect: signatureRect, placement: { prefer: 'above' }, children: _jsx(SignatureCard, { model: signature.model }) })), completion !== null && completionRect !== null && _jsx(CompletionPopup, { language: language, view: completion, rect: completionRect }), pick !== null && pickRect !== null && _jsx(PickPopup, { language: language, view: pick, rect: pickRect }), menu !== null && (_jsx(EditorContextMenu, { language: language, view: menu, children: contextMenuItems?.(language) })), symbols !== null && _jsx(SymbolPicker, { language: language, view: symbols }), peek !== null && _jsx(PeekPanel, { language: language, view: peek }), authors !== null && _jsx(CodeAuthorsCard, { language: language, view: authors }), rename !== null && renameRect !== null && _jsx(RenameCard, { language: language, view: rename, rect: renameRect, endRect: renameEnd })] }));
38
38
  }
@@ -26,4 +26,5 @@ export declare class CodeActionsFeature {
26
26
  apply(entry: ActionEntry): Promise<void>;
27
27
  private startRename;
28
28
  private fail;
29
+ private say;
29
30
  }
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { ErrorCodes, LspError, StaleResultError } from '@adecore/lsp';
3
2
  import { ACTION_GROUPS, REFACTOR_GROUPS, actionsOf, diagnosticsAt, fixableOnLine, isHint, mergeEntries, previewOf } from './code-actions-model.js';
4
3
  import { Refresher } from './refresher.js';
@@ -19,9 +18,6 @@ class ActionRefused extends Error {
19
18
  function isRefusal(error) {
20
19
  return error instanceof LspError && error.code === ErrorCodes.InternalError;
21
20
  }
22
- function say(key, options) {
23
- return i18next.t(`editor:language.actions.${key}`, options);
24
- }
25
21
  /*
26
22
  * What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
27
23
  * that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
@@ -39,12 +35,10 @@ export class CodeActionsFeature {
39
35
  resolved = new Map();
40
36
  constructor(language, timers = realTimers) {
41
37
  this.language = language;
42
- const { editor, project, uri } = language;
38
+ const { editor } = language;
43
39
  this.refresher = new Refresher((signal) => this.hint(signal), PAUSE_MS, timers);
44
- const providers = project.service.onProvidersChanged((changed) => {
45
- if (changed === uri) {
46
- this.refresher.later();
47
- }
40
+ const providers = language.onProvidersChanged(() => {
41
+ this.refresher.later();
48
42
  });
49
43
  const offs = [
50
44
  editor.onCaret(() => {
@@ -65,7 +59,7 @@ export class CodeActionsFeature {
65
59
  return false;
66
60
  }),
67
61
  language.diagnostics.onChange(() => this.refresher.later()),
68
- () => providers.dispose()
62
+ providers
69
63
  ];
70
64
  language.onDispose(() => {
71
65
  for (const off of offs) {
@@ -99,7 +93,7 @@ export class CodeActionsFeature {
99
93
  const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
100
94
  const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
101
95
  if (entry === undefined) {
102
- this.tell('success', say(entries === null ? 'unavailable' : 'organized'));
96
+ this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
103
97
  return;
104
98
  }
105
99
  await this.apply(entry);
@@ -107,13 +101,13 @@ export class CodeActionsFeature {
107
101
  async formatDocument() {
108
102
  const { editor, project, uri } = this.language;
109
103
  if (!project.service.supports('textDocument/formatting', uri)) {
110
- this.tell('error', say('noFormatter'));
104
+ this.tell('error', this.say('noFormatter'));
111
105
  return;
112
106
  }
113
107
  try {
114
108
  const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
115
109
  if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
116
- this.tell('error', say('readOnly'));
110
+ this.tell('error', this.say('readOnly'));
117
111
  }
118
112
  }
119
113
  catch (error) {
@@ -128,7 +122,7 @@ export class CodeActionsFeature {
128
122
  }
129
123
  async ask(options) {
130
124
  if (!this.supported) {
131
- this.tell('error', say('unavailable'));
125
+ this.tell('error', this.say('unavailable'));
132
126
  return;
133
127
  }
134
128
  // The press on a card or the gutter may have taken the focus, and the list answers to the editor's keys.
@@ -147,13 +141,13 @@ export class CodeActionsFeature {
147
141
  return;
148
142
  }
149
143
  if (entries.length === 0) {
150
- this.tell('success', say('none'));
144
+ this.tell('success', this.say('none'));
151
145
  return;
152
146
  }
153
147
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
154
148
  const groups = (options.groups ?? ACTION_GROUPS).flatMap((group) => {
155
149
  const rows = entries.filter((entry) => entry.group === group).map((entry) => ({ id: entry.id, label: entry.action.title, detail: '' }));
156
- return rows.length === 0 ? [] : [{ title: say(`groups.${group}`), rows }];
150
+ return rows.length === 0 ? [] : [{ title: this.say(`groups.${group}`), rows }];
157
151
  });
158
152
  this.language.completion.close();
159
153
  this.language.pick.open({
@@ -220,7 +214,7 @@ export class CodeActionsFeature {
220
214
  if (signal.aborted || this.asking) {
221
215
  return;
222
216
  }
223
- editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: say('show') } : null);
217
+ editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: this.say('show') } : null);
224
218
  }
225
219
  async resolve(entry, signal) {
226
220
  const known = this.resolved.get(entry);
@@ -267,9 +261,9 @@ export class CodeActionsFeature {
267
261
  return null;
268
262
  }
269
263
  const notes = [
270
- ...(found.hiddenLines > 0 ? [say('moreLines', { count: found.hiddenLines })] : []),
271
- ...(found.otherFiles > 0 ? [say('otherFiles', { count: found.otherFiles })] : []),
272
- ...(found.moves > 0 ? [say('moves', { count: found.moves })] : [])
264
+ ...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
265
+ ...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
266
+ ...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
273
267
  ];
274
268
  return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
275
269
  }
@@ -294,7 +288,7 @@ export class CodeActionsFeature {
294
288
  if (action.edit !== undefined) {
295
289
  const result = await project.applyWorkspaceEdit(action.edit);
296
290
  if (!result.applied) {
297
- this.tell('error', say('failed', { message: result.failureReason ?? say('refused') }));
291
+ this.tell('error', this.say('failed', { message: result.failureReason ?? this.say('refused') }));
298
292
  return;
299
293
  }
300
294
  }
@@ -323,7 +317,10 @@ export class CodeActionsFeature {
323
317
  }
324
318
  fail(error) {
325
319
  if (!(error instanceof StaleResultError)) {
326
- this.tell('error', say('failed', { message: error instanceof Error ? error.message : String(error) }));
320
+ this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
327
321
  }
328
322
  }
323
+ say(key, options) {
324
+ return this.language.project.i18n.t(`editor:language.actions.${key}`, options);
325
+ }
329
326
  }
@@ -1,3 +1,4 @@
1
+ import type { i18n } from 'i18next';
1
2
  import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
2
3
  export declare const UNCOMMITTED = -1;
3
4
  export declare function mapBlame(blame: GitBlameResult, base: string, current: readonly string[]): Int32Array | null;
@@ -14,4 +15,4 @@ export interface CodeAuthorship {
14
15
  }
15
16
  export declare function shortName(name: string): string;
16
17
  export declare function authorshipOf(commits: readonly GitBlameCommit[], mapped: Int32Array, lines: readonly string[], from: number, to: number): CodeAuthorship;
17
- export declare function authorsText(authorship: CodeAuthorship): string;
18
+ export declare function authorsText(authorship: CodeAuthorship, i18n: i18n): string;
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { diffLines, splitLines } from '@adecore/merge';
3
2
  import { formatNumber } from '@adecore/ui/format';
4
3
  /* What a line of the text in the editor is: the index of the commit that wrote it, or this for a line no commit holds yet. */
@@ -70,10 +69,10 @@ export function authorshipOf(commits, mapped, lines, from, to) {
70
69
  return { authors, uncommittedLines, modified, latest };
71
70
  }
72
71
  /* 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. */
73
- export function authorsText(authorship) {
72
+ export function authorsText(authorship, i18n) {
74
73
  const main = authorship.authors[0];
75
74
  if (main === undefined) {
76
- return i18next.t('editor:language.codeVision.newCode');
75
+ return i18n.t('editor:language.codeVision.newCode');
77
76
  }
78
77
  const others = authorship.authors.length - 1;
79
78
  return `${main.name}${others > 0 ? ` +${formatNumber(others)}` : ''}${authorship.modified ? ' *' : ''}`;
@@ -1,4 +1,5 @@
1
1
  import type { DocumentSymbolResult, Position } from '@adecore/lsp';
2
+ import type { i18n } from 'i18next';
2
3
  export declare const MAX_DECLARATIONS = 3000;
3
4
  export interface CodeVisionDeclaration {
4
5
  readonly id: string;
@@ -17,4 +18,4 @@ export interface DeclarationSource {
17
18
  }
18
19
  export declare function startAfterComments(source: DeclarationSource, from: Position, to: Position): Position | null;
19
20
  export declare function declarationsOf(result: DocumentSymbolResult, source: DeclarationSource): CodeVisionDeclaration[];
20
- export declare function usagesText(count: number): string;
21
+ export declare function usagesText(count: number, i18n: i18n): string;
@@ -1,5 +1,4 @@
1
1
  import { formatNumber } from '@adecore/ui/format';
2
- import i18next from 'i18next';
3
2
  /* 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. */
4
3
  export const MAX_DECLARATIONS = 3000;
5
4
  /* LSP's SymbolKind: the kinds a row is drawn for and the ones whose children can be declarations too. */
@@ -177,8 +176,8 @@ export function declarationsOf(result, source) {
177
176
  return declarations;
178
177
  }
179
178
  /* "No usages", "1 usage" and "N usages". */
180
- export function usagesText(count) {
179
+ export function usagesText(count, i18n) {
181
180
  return count === 0
182
- ? i18next.t('editor:language.codeVision.usagesNone')
183
- : i18next.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
181
+ ? i18n.t('editor:language.codeVision.usagesNone')
182
+ : i18n.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
184
183
  }
@@ -42,7 +42,7 @@ export class CodeVisionFeature {
42
42
  constructor(language, onSymbols, timers = realTimers) {
43
43
  this.language = language;
44
44
  this.timers = timers;
45
- const { editor, project, uri } = language;
45
+ const { editor } = language;
46
46
  const offs = [
47
47
  onSymbols((result) => this.read(result)),
48
48
  editor.onTextChange(() => {
@@ -54,19 +54,17 @@ export class CodeVisionFeature {
54
54
  this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
55
55
  })
56
56
  ];
57
- const providers = project.service.onProvidersChanged((changed) => {
58
- if (changed === uri) {
59
- this.failed.clear();
60
- this.publish();
61
- this.plan();
62
- }
57
+ const providers = language.onProvidersChanged(() => {
58
+ this.failed.clear();
59
+ this.publish();
60
+ this.plan();
63
61
  });
64
62
  language.onDispose(() => {
65
63
  this.disposed = true;
66
64
  for (const off of offs) {
67
65
  off();
68
66
  }
69
- providers.dispose();
67
+ providers();
70
68
  this.timers.clear(this.scrollTimer);
71
69
  for (const controller of this.running.values()) {
72
70
  controller.abort();
@@ -180,7 +178,11 @@ export class CodeVisionFeature {
180
178
  const entries = [];
181
179
  const known = this.counts.get(declaration.id);
182
180
  if (counted && declaration.usages && known !== undefined) {
183
- entries.push({ id: 'usages', text: usagesText(known.count), activate: () => void this.language.peek.open(declaration.position) });
181
+ entries.push({
182
+ id: 'usages',
183
+ text: usagesText(known.count, this.language.project.i18n),
184
+ activate: () => void this.language.peek.open(declaration.position)
185
+ });
184
186
  }
185
187
  if (mapped !== null && declaration.authors) {
186
188
  let authorship = this.authorships.get(declaration.id);
@@ -191,7 +193,7 @@ export class CodeVisionFeature {
191
193
  const shown = authorship;
192
194
  entries.push({
193
195
  id: 'authors',
194
- text: authorsText(shown),
196
+ text: authorsText(shown, this.language.project.i18n),
195
197
  ...(shown.authors.length > 0 ? { icon: shown.authors.length > 1 ? 'users' : 'user' } : {}),
196
198
  activate: (anchor) => this.showAuthors(declaration, shown, anchor)
197
199
  });
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError } from '@adecore/lsp';
3
2
  import { comparePositions, shiftPosition } from './diagnostics-model.js';
4
3
  import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
@@ -368,7 +367,7 @@ export class CompletionFeature {
368
367
  const asked = this.explicit && !refresh;
369
368
  this.refilter();
370
369
  if (asked && !this.isOpen && !this.incomplete) {
371
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.completion.none') });
370
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
372
371
  }
373
372
  else if (asked && this.ranked.length === 1 && !this.incomplete) {
374
373
  // One answer to a list that was asked for is the one wanted.
@@ -59,6 +59,7 @@ export declare class EditorLanguage {
59
59
  jump(position: EditorPosition): void;
60
60
  visit(place: Place): void;
61
61
  locations(places: readonly Location[], anchor: EditorPosition, title: string): void;
62
+ onProvidersChanged(listener: () => void): () => void;
62
63
  onDispose(dispose: () => void): void;
63
64
  dispose(): void;
64
65
  }
@@ -131,6 +131,28 @@ export class EditorLanguage {
131
131
  const rows = places.map((place, index) => ({ id: String(index), ...locationRow(place, this.project.pathOfLocation(place.uri)) }));
132
132
  this.pick.open({ anchor, title, groups: [{ title: null, rows }], accept: (id) => this.goTo(places[Number(id)]) });
133
133
  }
134
+ /*
135
+ * Runs `listener` once the document is open and again whenever the service says what it can ask for
136
+ * this document changed. A service that is ready before the document opens never says so, so the
137
+ * opening counts as the first change.
138
+ */
139
+ onProvidersChanged(listener) {
140
+ let listening = true;
141
+ void this.document.ready.then(() => {
142
+ if (listening && !this.disposed) {
143
+ listener();
144
+ }
145
+ }, () => undefined);
146
+ const providers = this.project.service.onProvidersChanged((uri) => {
147
+ if (uri === this.uri) {
148
+ listener();
149
+ }
150
+ });
151
+ return () => {
152
+ listening = false;
153
+ providers.dispose();
154
+ };
155
+ }
134
156
  /* Runs `dispose` when the editor lets go of its document. */
135
157
  onDispose(dispose) {
136
158
  this.disposers.push(dispose);
package/dist/folding.js CHANGED
@@ -77,15 +77,13 @@ export class FoldingFeature {
77
77
  this.publish();
78
78
  });
79
79
  const edits = editor.onTextChange(() => this.refresher.later());
80
- const providers = project.service.onProvidersChanged((changed) => {
81
- if (changed === uri) {
82
- this.refresher.now();
83
- }
80
+ const providers = language.onProvidersChanged(() => {
81
+ this.refresher.now();
84
82
  });
85
83
  language.onDispose(() => {
86
84
  symbols();
87
85
  edits();
88
- providers.dispose();
86
+ providers();
89
87
  this.refresher.dispose();
90
88
  editor.setFoldHints(null);
91
89
  });
package/dist/history.js CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { isShortcut } from './shortcut-keys.js';
3
2
  /* Back, Forward and the list of recent locations over the history the whole project shares. */
4
3
  export class HistoryFeature {
@@ -42,6 +41,6 @@ export class HistoryFeature {
42
41
  /* Lists the places of the history under the caret; choosing one jumps there like any other jump. */
43
42
  recent() {
44
43
  const places = this.language.project.history.recent(this.language.place);
45
- this.language.locations(places.map((place) => ({ uri: place.uri, range: { start: place.position, end: place.position } })), this.language.editor.getCaret(), i18next.t('editor:language.history.recent'));
44
+ this.language.locations(places.map((place) => ({ uri: place.uri, range: { start: place.position, end: place.position } })), this.language.editor.getCaret(), this.language.project.i18n.t('editor:language.history.recent'));
46
45
  }
47
46
  }
@@ -1,3 +1,4 @@
1
+ import type { i18n } from 'i18next';
1
2
  import type { Keymap } from '@adecore/editor/keymap';
2
3
  import type { ProjectFiles } from './workspace-edit.ts';
3
4
  import type { Place } from './navigation-history.ts';
@@ -31,6 +32,7 @@ export interface RenameSuggestionsRequest {
31
32
  uses: readonly string[];
32
33
  }
33
34
  export interface LanguageHost {
35
+ readonly i18n?: i18n;
34
36
  readonly folder?: string;
35
37
  readonly files?: ProjectFiles;
36
38
  readonly apple?: boolean;
package/dist/hover.js CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { rangeHolds } from './diagnostics-model.js';
3
2
  import { hoverTextOf, isEmptyHover, locationsOf } from './hover-content.js';
4
3
  import { wordRangeAt } from './rename-model.js';
@@ -197,7 +196,7 @@ export class HoverFeature {
197
196
  if (problems.length === 0 && info === null) {
198
197
  this.hide();
199
198
  if (pinned) {
200
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.hover.none') });
199
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.hover.none') });
201
200
  }
202
201
  return;
203
202
  }
@@ -57,15 +57,13 @@ export class InlayHintsFeature {
57
57
  this.refresher.later();
58
58
  }
59
59
  });
60
- const providers = project.service.onProvidersChanged((changed) => {
61
- if (changed === uri) {
62
- this.refresher.now();
63
- }
60
+ const providers = language.onProvidersChanged(() => {
61
+ this.refresher.now();
64
62
  });
65
63
  language.onDispose(() => {
66
64
  edits();
67
65
  scrolls();
68
- providers.dispose();
66
+ providers();
69
67
  this.refresher.dispose();
70
68
  editor.setInlayHints([]);
71
69
  });
@@ -14,6 +14,7 @@ export declare class NavigationFeature {
14
14
  go(kind: NavigationKind, position?: EditorPosition): Promise<void>;
15
15
  goToName(name: string, anchor: EditorPosition, own: Location | null): Promise<void>;
16
16
  private tell;
17
+ private say;
17
18
  }
18
19
  export declare function locationRow(location: Location, stored: string | null): {
19
20
  label: string;