@adecore/editor-react 0.17.0-beta.1 → 0.17.0

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 (68) 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/HoverCard.d.ts +6 -0
  6. package/dist/HoverCard.js +20 -5
  7. package/dist/LanguagePopups.d.ts +3 -1
  8. package/dist/LanguagePopups.js +3 -3
  9. package/dist/code-actions-model.js +1 -1
  10. package/dist/code-actions.d.ts +1 -0
  11. package/dist/code-actions.js +19 -22
  12. package/dist/code-authors.d.ts +2 -1
  13. package/dist/code-authors.js +2 -3
  14. package/dist/code-vision-model.d.ts +2 -1
  15. package/dist/code-vision-model.js +3 -4
  16. package/dist/code-vision.js +12 -10
  17. package/dist/completion.js +1 -2
  18. package/dist/editor-language.d.ts +1 -0
  19. package/dist/editor-language.js +22 -0
  20. package/dist/folding.d.ts +2 -4
  21. package/dist/folding.js +11 -7
  22. package/dist/history.js +1 -2
  23. package/dist/host-types.d.ts +2 -0
  24. package/dist/hover.js +1 -2
  25. package/dist/inlay-hints.js +3 -5
  26. package/dist/language-ids.js +4 -2
  27. package/dist/locales/en.json +1 -0
  28. package/dist/locales/nl.json +1 -0
  29. package/dist/navigation.d.ts +1 -0
  30. package/dist/navigation.js +8 -9
  31. package/dist/peek.d.ts +1 -0
  32. package/dist/peek.js +6 -7
  33. package/dist/project-language.d.ts +2 -0
  34. package/dist/project-language.js +3 -0
  35. package/dist/rename.d.ts +1 -0
  36. package/dist/rename.js +10 -11
  37. package/dist/semantic-tokens.js +3 -5
  38. package/dist/symbol-picker.js +2 -3
  39. package/dist/symbols.js +3 -5
  40. package/dist/workspace-edit.d.ts +1 -0
  41. package/dist/workspace-edit.js +46 -7
  42. package/package.json +5 -5
  43. package/src/EditorView.tsx +5 -3
  44. package/src/HoverCard.tsx +44 -25
  45. package/src/LanguagePopups.tsx +8 -4
  46. package/src/code-actions-model.ts +1 -1
  47. package/src/code-actions.ts +20 -23
  48. package/src/code-authors.ts +3 -3
  49. package/src/code-vision-model.ts +4 -4
  50. package/src/code-vision.ts +12 -10
  51. package/src/completion.ts +1 -2
  52. package/src/editor-language.ts +26 -0
  53. package/src/folding.ts +14 -10
  54. package/src/history.ts +1 -2
  55. package/src/host-types.ts +3 -0
  56. package/src/hover.ts +1 -2
  57. package/src/inlay-hints.ts +3 -5
  58. package/src/language-ids.ts +4 -2
  59. package/src/locales/en.json +1 -0
  60. package/src/locales/nl.json +1 -0
  61. package/src/navigation.ts +9 -10
  62. package/src/peek.ts +7 -8
  63. package/src/project-language.ts +3 -0
  64. package/src/rename.ts +11 -12
  65. package/src/semantic-tokens.ts +3 -5
  66. package/src/symbol-picker.ts +2 -3
  67. package/src/symbols.ts +3 -5
  68. package/src/workspace-edit.ts +70 -10
@@ -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
  });
@@ -15,7 +15,8 @@ const LSP_IDS = {
15
15
  yaml: 'yaml',
16
16
  python: 'python',
17
17
  shellscript: 'shellscript',
18
- docker: 'dockerfile'
18
+ docker: 'dockerfile',
19
+ twig: 'twig'
19
20
  };
20
21
  /* The LSP language id of a file, or null when no language server here knows its language. */
21
22
  export function lspLanguageIdOf(language) {
@@ -41,7 +42,8 @@ const PATH_IDS = {
41
42
  css: 'css',
42
43
  scss: 'scss',
43
44
  html: 'html',
44
- md: 'markdown'
45
+ md: 'markdown',
46
+ twig: 'twig'
45
47
  };
46
48
  /* The highlighter id for the code of a file we only know by its path, such as a place another file refers to. */
47
49
  export function shikiLanguageOfPath(path) {
@@ -6,6 +6,7 @@
6
6
  "definition": "Go to definition",
7
7
  "references_one": "{{formatted}} reference",
8
8
  "references_other": "{{formatted}} references",
9
+ "line": "Line {{line}}",
9
10
  "none": "No information for this position"
10
11
  },
11
12
  "edit": {
@@ -6,6 +6,7 @@
6
6
  "definition": "Ga naar definitie",
7
7
  "references_one": "{{formatted}} referentie",
8
8
  "references_other": "{{formatted}} referenties",
9
+ "line": "Regel {{line}}",
9
10
  "none": "Geen informatie voor deze plek"
10
11
  },
11
12
  "edit": {
@@ -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;
@@ -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
  });
@@ -11,6 +11,7 @@ export interface ProjectFiles {
11
11
  stage(files: readonly StagedFile[]): void;
12
12
  save(files: readonly StagedFile[]): Promise<string | null>;
13
13
  rename(from: string, to: string): Promise<string | null>;
14
+ create?(path: string, text: string): Promise<string | null>;
14
15
  }
15
16
  export interface WorkspaceEditHost {
16
17
  editorOf(uri: string): Editor | undefined;
@@ -2,13 +2,16 @@ import { applyTextEdits, fileUriToPath } from '@adecore/lsp';
2
2
  function refused(failureReason) {
3
3
  return { applied: false, failureReason };
4
4
  }
5
- /* The steps of an edit; null when it creates or deletes a file, which an edit is not applied with. */
5
+ /* The steps of an edit; null when it deletes a file, which an edit is not applied with. */
6
6
  function stepsOf(edit) {
7
7
  const steps = [];
8
8
  for (const change of edit.documentChanges ?? []) {
9
9
  if ('textDocument' in change) {
10
10
  steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
11
11
  }
12
+ else if (change.kind === 'create') {
13
+ steps.push({ kind: 'create', file: change });
14
+ }
12
15
  else if (change.kind === 'rename') {
13
16
  steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
14
17
  }
@@ -23,7 +26,7 @@ function stepsOf(edit) {
23
26
  }
24
27
  return steps;
25
28
  }
26
- /* The text edits of an edit per document, as the entries a document has in it; null when it creates or deletes a file. */
29
+ /* The text edits of an edit per document, as the entries a document has in it; null when it deletes a file. */
27
30
  export function entriesOf(edit) {
28
31
  const steps = stepsOf(edit);
29
32
  if (steps === null) {
@@ -48,13 +51,14 @@ function contentChangeOf(edit) {
48
51
  * Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
49
52
  * its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
50
53
  * nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
51
- * move are then one change, so every file is saved through the machine and the move is made there. The
52
- * edit is checked against every text first, so one that does not fit changes nothing.
54
+ * move are then one change, so every file is saved through the machine and the move is made there. A file
55
+ * the edit creates is made through the machine with the text the edit writes into it. The edit is checked
56
+ * against every text first, so one that does not fit changes nothing.
53
57
  */
54
58
  export async function applyWorkspaceEdit(edit, host) {
55
59
  const steps = stepsOf(edit);
56
60
  if (steps === null) {
57
- return refused('Creating and deleting files is not supported yet');
61
+ return refused('Deleting files is not supported yet');
58
62
  }
59
63
  const planned = await plan(steps, host);
60
64
  if (typeof planned === 'string') {
@@ -63,8 +67,8 @@ export async function applyWorkspaceEdit(edit, host) {
63
67
  const moving = planned.some((item) => item.kind === 'rename');
64
68
  const staged = [];
65
69
  for (const item of planned) {
66
- if (item.kind === 'rename') {
67
- const reason = await host.files.rename(item.oldPath, item.newPath);
70
+ if (item.kind === 'rename' || item.kind === 'create') {
71
+ const reason = item.kind === 'rename' ? await host.files.rename(item.oldPath, item.newPath) : await host.files.create(item.path, item.text);
68
72
  if (reason !== null) {
69
73
  return refused(reason);
70
74
  }
@@ -105,8 +109,21 @@ async function plan(steps, host) {
105
109
  const texts = new Map();
106
110
  // A file that has moved is read where it still stands: nothing moves until the plan is made.
107
111
  const origins = new Map();
112
+ // A file that is made is written once, with what every step leaves in it.
113
+ const creates = new Map();
108
114
  const planned = [];
109
115
  for (const step of steps) {
116
+ if (step.kind === 'create') {
117
+ const made = await planCreate(step.file, host, texts);
118
+ if (typeof made === 'string') {
119
+ return made;
120
+ }
121
+ if (made !== null) {
122
+ creates.set(step.file.uri, made);
123
+ planned.push(made);
124
+ }
125
+ continue;
126
+ }
110
127
  if (step.kind === 'rename') {
111
128
  const oldPath = fileUriToPath(step.oldUri);
112
129
  const newPath = fileUriToPath(step.newUri);
@@ -118,6 +135,16 @@ async function plan(steps, host) {
118
135
  continue;
119
136
  }
120
137
  const source = origins.get(step.uri) ?? step.uri;
138
+ const created = creates.get(source);
139
+ if (created !== undefined) {
140
+ try {
141
+ created.text = applyTextEdits(created.text, step.edits);
142
+ }
143
+ catch (error) {
144
+ return error instanceof Error ? error.message : String(error);
145
+ }
146
+ continue;
147
+ }
121
148
  if (!texts.has(source)) {
122
149
  const editor = host.editorOf(source);
123
150
  if (editor) {
@@ -145,3 +172,15 @@ async function plan(steps, host) {
145
172
  }
146
173
  return planned;
147
174
  }
175
+ /* A file that is not there yet, or null when it is and the create says to leave it. Making one over a file that is there is not supported. */
176
+ async function planCreate(file, host, texts) {
177
+ const path = fileUriToPath(file.uri);
178
+ if (path === null || host.files?.create === undefined) {
179
+ return `Creating ${file.uri} is not supported here`;
180
+ }
181
+ const there = texts.has(file.uri) || host.editorOf(file.uri) !== undefined || (await host.files.read(path)) !== null;
182
+ if (!there) {
183
+ return { kind: 'create', path, text: '' };
184
+ }
185
+ return file.options?.ignoreIfExists === true && file.options.overwrite !== true ? null : `${path} already exists`;
186
+ }
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",
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",
61
+ "@adecore/lsp": "0.17.0",
62
+ "@adecore/merge": "0.17.0",
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",
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
  }