@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
package/src/peek.ts CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError, type Location, type Range } from '@adecore/lsp';
3
2
  import type { EditorPosition } from '@adecore/editor';
4
3
  import type { EditorLanguage } from './editor-language.ts';
@@ -22,10 +21,6 @@ function placeKey(location: Location): string {
22
21
  return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
23
22
  }
24
23
 
25
- function say(key: string, options?: Record<string, unknown>): string {
26
- return i18next.t(`editor:language.peek.${key}`, options);
27
- }
28
-
29
24
  /*
30
25
  * The references of the name at the caret, between the lines of the file under the line it is on. The
31
26
  * editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
@@ -75,7 +70,7 @@ export class PeekFeature {
75
70
  const { editor, project, uri } = this.language;
76
71
  const method = KINDS[kind].method;
77
72
  if (!project.service.supports(method, uri)) {
78
- this.tell(say(KINDS[kind].unavailable));
73
+ this.tell(this.say(KINDS[kind].unavailable));
79
74
  return;
80
75
  }
81
76
  const token = ++this.token;
@@ -84,7 +79,7 @@ export class PeekFeature {
84
79
  found = await this.fetch(kind, position);
85
80
  } catch (error) {
86
81
  if (!(error instanceof StaleResultError)) {
87
- this.tell(say('failed', { message: error instanceof Error ? error.message : String(error) }));
82
+ this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
88
83
  }
89
84
  return;
90
85
  }
@@ -102,7 +97,7 @@ export class PeekFeature {
102
97
  ) === index
103
98
  );
104
99
  if (unique.length === 0) {
105
- this.tell(say(KINDS[kind].none));
100
+ this.tell(this.say(KINDS[kind].none));
106
101
  return;
107
102
  }
108
103
  const uris = [...new Set(unique.map((location) => location.uri))].slice(0, PEEK_READ_FILES);
@@ -245,4 +240,8 @@ export class PeekFeature {
245
240
  private tell(title: string): void {
246
241
  this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
247
242
  }
243
+
244
+ private say(key: string, options?: Record<string, unknown>): string {
245
+ return this.language.project.i18n.t(`editor:language.peek.${key}`, options);
246
+ }
248
247
  }
@@ -1,3 +1,4 @@
1
+ import i18next, { type i18n } from 'i18next';
1
2
  import { fileUriToPath, type ApplyWorkspaceEditResult, type ContentChange, 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';
@@ -78,6 +79,7 @@ interface Holder {
78
79
  export class ProjectLanguage {
79
80
  readonly service: LanguageService;
80
81
  readonly host: LanguageHost;
82
+ readonly i18n: i18n;
81
83
  readonly folder: string;
82
84
  readonly problems: ProjectProblems;
83
85
  readonly history = new NavigationHistory();
@@ -90,6 +92,7 @@ export class ProjectLanguage {
90
92
  constructor(service: LanguageService, host: LanguageHost = {}) {
91
93
  this.service = service;
92
94
  this.host = host;
95
+ this.i18n = host.i18n ?? i18next;
93
96
  this.folder = host.folder ?? '';
94
97
  this.files = host.files ?? null;
95
98
  this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
package/src/rename.ts CHANGED
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError, type Location, type PrepareRenameResult, type WorkspaceEdit } from '@adecore/lsp';
3
2
  import type { EditorRange } from '@adecore/editor';
4
3
  import type { EditorLanguage } from './editor-language.ts';
@@ -13,10 +12,6 @@ const SAMPLE_LOCATIONS = 8;
13
12
  const SAMPLE_OTHER_FILES = 3;
14
13
  const CONTEXT_LINES = 3;
15
14
 
16
- function say(key: string, options?: Record<string, unknown>): string {
17
- return i18next.t(`editor:language.rename.${key}`, options);
18
- }
19
-
20
15
  interface Session {
21
16
  readonly target: RenameTarget;
22
17
  readonly original: string;
@@ -67,7 +62,7 @@ export class RenameFeature {
67
62
  async start(): Promise<void> {
68
63
  const { editor, project, uri } = this.language;
69
64
  if (!project.service.supports(METHOD, uri)) {
70
- this.tell('error', say('unavailable'));
65
+ this.tell('error', this.say('unavailable'));
71
66
  return;
72
67
  }
73
68
  this.cancel();
@@ -78,7 +73,7 @@ export class RenameFeature {
78
73
  answer = prepare ? await project.service.prepareRename(uri, caret) : { defaultBehavior: true };
79
74
  } catch (error) {
80
75
  if (!(error instanceof StaleResultError)) {
81
- this.tell('error', say('nothing'));
76
+ this.tell('error', this.say('nothing'));
82
77
  }
83
78
  return;
84
79
  }
@@ -92,7 +87,7 @@ export class RenameFeature {
92
87
  )
93
88
  );
94
89
  if (target === null) {
95
- this.tell('error', say('nothing'));
90
+ this.tell('error', this.say('nothing'));
96
91
  return;
97
92
  }
98
93
  const session: Session = { target, original: editor.textInRange(target.range), edit: null, names: new AbortController() };
@@ -120,7 +115,7 @@ export class RenameFeature {
120
115
  return;
121
116
  }
122
117
  if (edit === null) {
123
- this.fail(say('refused'));
118
+ this.fail(this.say('refused'));
124
119
  return;
125
120
  }
126
121
  session.edit = edit;
@@ -140,7 +135,7 @@ export class RenameFeature {
140
135
  this.publish({ phase: 'preview', busy: false, name: next, files });
141
136
  } catch (error) {
142
137
  if (this.session === session) {
143
- this.fail(error instanceof StaleResultError ? say('stale') : error instanceof Error ? error.message : String(error));
138
+ this.fail(error instanceof StaleResultError ? this.say('stale') : error instanceof Error ? error.message : String(error));
144
139
  }
145
140
  }
146
141
  }
@@ -168,7 +163,7 @@ export class RenameFeature {
168
163
  if (result.applied) {
169
164
  this.finish();
170
165
  } else {
171
- this.fail(result.failureReason ?? say('refused'));
166
+ this.fail(result.failureReason ?? this.say('refused'));
172
167
  }
173
168
  }
174
169
 
@@ -176,7 +171,7 @@ export class RenameFeature {
176
171
  private fail(message: string): void {
177
172
  if (this.session !== null) {
178
173
  this.session.edit = null;
179
- this.publish({ phase: 'input', busy: false, files: [], error: say('failed', { message }) });
174
+ this.publish({ phase: 'input', busy: false, files: [], error: this.say('failed', { message }) });
180
175
  }
181
176
  }
182
177
 
@@ -288,4 +283,8 @@ export class RenameFeature {
288
283
  }
289
284
  });
290
285
  }
286
+
287
+ private say(key: string, options?: Record<string, unknown>): string {
288
+ return this.language.project.i18n.t(`editor:language.rename.${key}`, options);
289
+ }
291
290
  }
@@ -33,14 +33,12 @@ export class SemanticTokensFeature {
33
33
  timers
34
34
  );
35
35
  const edits = editor.onTextChange(() => this.refresher.later());
36
- const providers = project.service.onProvidersChanged((changed) => {
37
- if (changed === uri) {
38
- this.refresher.now();
39
- }
36
+ const providers = language.onProvidersChanged(() => {
37
+ this.refresher.now();
40
38
  });
41
39
  language.onDispose(() => {
42
40
  edits();
43
- providers.dispose();
41
+ providers();
44
42
  this.refresher.dispose();
45
43
  editor.setSemanticTokens(null);
46
44
  });
@@ -1,4 +1,3 @@
1
- import i18next from 'i18next';
2
1
  import { StaleResultError, type Location } from '@adecore/lsp';
3
2
  import { basenameOf } from './paths.ts';
4
3
  import type { EditorLanguage } from './editor-language.ts';
@@ -33,7 +32,7 @@ export class SymbolPickerFeature {
33
32
  async open(): Promise<void> {
34
33
  const { project, uri, popups } = this.language;
35
34
  if (!project.service.supports('textDocument/documentSymbol', uri)) {
36
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.symbols.unavailable') });
35
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.symbols.unavailable') });
37
36
  return;
38
37
  }
39
38
  try {
@@ -44,7 +43,7 @@ export class SymbolPickerFeature {
44
43
  this.language.project.host.notify?.({
45
44
  id: TOAST_ID,
46
45
  kind: 'error',
47
- title: i18next.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
46
+ title: this.language.project.i18n.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
48
47
  });
49
48
  }
50
49
  }
package/src/symbols.ts CHANGED
@@ -89,14 +89,12 @@ export class SymbolsFeature {
89
89
  timers
90
90
  );
91
91
  const edits = editor.onTextChange(() => this.refresher.later());
92
- const providers = project.service.onProvidersChanged((changed) => {
93
- if (changed === uri) {
94
- this.refresher.now();
95
- }
92
+ const providers = language.onProvidersChanged(() => {
93
+ this.refresher.now();
96
94
  });
97
95
  language.onDispose(() => {
98
96
  edits();
99
- providers.dispose();
97
+ providers();
100
98
  this.refresher.dispose();
101
99
  editor.setBlocks(null);
102
100
  });
@@ -1,5 +1,13 @@
1
1
  import type { Editor, EditorContentChange } from '@adecore/editor';
2
- import { applyTextEdits, fileUriToPath, type ApplyWorkspaceEditResult, type RenameFile, type TextEdit, type WorkspaceEdit } from '@adecore/lsp';
2
+ import {
3
+ applyTextEdits,
4
+ fileUriToPath,
5
+ type ApplyWorkspaceEditResult,
6
+ type CreateFile,
7
+ type RenameFile,
8
+ type TextEdit,
9
+ type WorkspaceEdit
10
+ } from '@adecore/lsp';
3
11
  import type { DiskText } from './host-types.ts';
4
12
 
5
13
  /* A file that changed without an editor of this project holding it: its new text waits as an unsaved draft. */
@@ -18,6 +26,8 @@ export interface ProjectFiles {
18
26
  save(files: readonly StagedFile[]): Promise<string | null>;
19
27
  /* Moves a file or folder on the machine, with what is open on it, and tells the servers it moved; the reason it could not, or null. */
20
28
  rename(from: string, to: string): Promise<string | null>;
29
+ /* Makes a file that is not there with its text, through the machine; the reason it could not, or null. Without it no edit creates a file. */
30
+ create?(path: string, text: string): Promise<string | null>;
21
31
  }
22
32
 
23
33
  export interface WorkspaceEditHost {
@@ -30,17 +40,20 @@ function refused(failureReason: string): ApplyWorkspaceEditResult {
30
40
  return { applied: false, failureReason };
31
41
  }
32
42
 
33
- /* One thing an edit does, in the order it says: text edits of a document, or a file that moves. */
43
+ /* One thing an edit does, in the order it says: text edits of a document, a file that is made, or a file that moves. */
34
44
  type Step =
35
45
  | { readonly kind: 'edits'; readonly uri: string; readonly edits: TextEdit[] }
46
+ | { readonly kind: 'create'; readonly file: CreateFile }
36
47
  | { readonly kind: 'rename'; readonly oldUri: string; readonly newUri: string };
37
48
 
38
- /* The steps of an edit; null when it creates or deletes a file, which an edit is not applied with. */
49
+ /* The steps of an edit; null when it deletes a file, which an edit is not applied with. */
39
50
  function stepsOf(edit: WorkspaceEdit): Step[] | null {
40
51
  const steps: Step[] = [];
41
52
  for (const change of edit.documentChanges ?? []) {
42
53
  if ('textDocument' in change) {
43
54
  steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
55
+ } else if (change.kind === 'create') {
56
+ steps.push({ kind: 'create', file: change });
44
57
  } else if (change.kind === 'rename') {
45
58
  steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
46
59
  } else {
@@ -55,7 +68,7 @@ function stepsOf(edit: WorkspaceEdit): Step[] | null {
55
68
  return steps;
56
69
  }
57
70
 
58
- /* The text edits of an edit per document, as the entries a document has in it; null when it creates or deletes a file. */
71
+ /* The text edits of an edit per document, as the entries a document has in it; null when it deletes a file. */
59
72
  export function entriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> | null {
60
73
  const steps = stepsOf(edit);
61
74
  if (steps === null) {
@@ -90,19 +103,27 @@ interface PlannedEdits {
90
103
  readonly disk: DiskText | null;
91
104
  }
92
105
 
93
- type Planned = PlannedEdits | { readonly kind: 'rename'; readonly oldPath: string; readonly newPath: string };
106
+ /* A file to make, with the text every step of the edit leaves in it. */
107
+ interface PlannedCreate {
108
+ readonly kind: 'create';
109
+ readonly path: string;
110
+ text: string;
111
+ }
112
+
113
+ type Planned = PlannedEdits | PlannedCreate | { readonly kind: 'rename'; readonly oldPath: string; readonly newPath: string };
94
114
 
95
115
  /*
96
116
  * Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
97
117
  * its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
98
118
  * nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
99
- * move are then one change, so every file is saved through the machine and the move is made there. The
100
- * edit is checked against every text first, so one that does not fit changes nothing.
119
+ * move are then one change, so every file is saved through the machine and the move is made there. A file
120
+ * the edit creates is made through the machine with the text the edit writes into it. The edit is checked
121
+ * against every text first, so one that does not fit changes nothing.
101
122
  */
102
123
  export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult> {
103
124
  const steps = stepsOf(edit);
104
125
  if (steps === null) {
105
- return refused('Creating and deleting files is not supported yet');
126
+ return refused('Deleting files is not supported yet');
106
127
  }
107
128
  const planned = await plan(steps, host);
108
129
  if (typeof planned === 'string') {
@@ -111,8 +132,8 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEdi
111
132
  const moving = planned.some((item) => item.kind === 'rename');
112
133
  const staged: StagedFile[] = [];
113
134
  for (const item of planned) {
114
- if (item.kind === 'rename') {
115
- const reason = await host.files!.rename(item.oldPath, item.newPath);
135
+ if (item.kind === 'rename' || item.kind === 'create') {
136
+ const reason = item.kind === 'rename' ? await host.files!.rename(item.oldPath, item.newPath) : await host.files!.create!(item.path, item.text);
116
137
  if (reason !== null) {
117
138
  return refused(reason);
118
139
  }
@@ -154,8 +175,21 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
154
175
  const texts = new Map<string, { text: string; disk: DiskText | null }>();
155
176
  // A file that has moved is read where it still stands: nothing moves until the plan is made.
156
177
  const origins = new Map<string, string>();
178
+ // A file that is made is written once, with what every step leaves in it.
179
+ const creates = new Map<string, PlannedCreate>();
157
180
  const planned: Planned[] = [];
158
181
  for (const step of steps) {
182
+ if (step.kind === 'create') {
183
+ const made = await planCreate(step.file, host, texts);
184
+ if (typeof made === 'string') {
185
+ return made;
186
+ }
187
+ if (made !== null) {
188
+ creates.set(step.file.uri, made);
189
+ planned.push(made);
190
+ }
191
+ continue;
192
+ }
159
193
  if (step.kind === 'rename') {
160
194
  const oldPath = fileUriToPath(step.oldUri);
161
195
  const newPath = fileUriToPath(step.newUri);
@@ -167,6 +201,15 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
167
201
  continue;
168
202
  }
169
203
  const source = origins.get(step.uri) ?? step.uri;
204
+ const created = creates.get(source);
205
+ if (created !== undefined) {
206
+ try {
207
+ created.text = applyTextEdits(created.text, step.edits);
208
+ } catch (error) {
209
+ return error instanceof Error ? error.message : String(error);
210
+ }
211
+ continue;
212
+ }
170
213
  if (!texts.has(source)) {
171
214
  const editor = host.editorOf(source);
172
215
  if (editor) {
@@ -192,3 +235,20 @@ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Pl
192
235
  }
193
236
  return planned;
194
237
  }
238
+
239
+ /* 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. */
240
+ async function planCreate(
241
+ file: CreateFile,
242
+ host: WorkspaceEditHost,
243
+ texts: ReadonlyMap<string, { text: string; disk: DiskText | null }>
244
+ ): Promise<PlannedCreate | null | string> {
245
+ const path = fileUriToPath(file.uri);
246
+ if (path === null || host.files?.create === undefined) {
247
+ return `Creating ${file.uri} is not supported here`;
248
+ }
249
+ const there = texts.has(file.uri) || host.editorOf(file.uri) !== undefined || (await host.files.read(path)) !== null;
250
+ if (!there) {
251
+ return { kind: 'create', path, text: '' };
252
+ }
253
+ return file.options?.ignoreIfExists === true && file.options.overwrite !== true ? null : `${path} already exists`;
254
+ }