@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.
- package/LICENSE +1 -1
- package/README.md +35 -30
- package/dist/EditorView.d.ts +3 -2
- package/dist/EditorView.js +2 -2
- package/dist/HoverCard.d.ts +6 -0
- package/dist/HoverCard.js +20 -5
- package/dist/LanguagePopups.d.ts +3 -1
- package/dist/LanguagePopups.js +3 -3
- package/dist/code-actions-model.js +1 -1
- package/dist/code-actions.d.ts +1 -0
- package/dist/code-actions.js +19 -22
- package/dist/code-authors.d.ts +2 -1
- package/dist/code-authors.js +2 -3
- package/dist/code-vision-model.d.ts +2 -1
- package/dist/code-vision-model.js +3 -4
- package/dist/code-vision.js +12 -10
- package/dist/completion.js +1 -2
- package/dist/editor-language.d.ts +1 -0
- package/dist/editor-language.js +22 -0
- package/dist/folding.d.ts +2 -4
- package/dist/folding.js +11 -7
- package/dist/history.js +1 -2
- package/dist/host-types.d.ts +2 -0
- package/dist/hover.js +1 -2
- package/dist/inlay-hints.js +3 -5
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +1 -0
- package/dist/locales/nl.json +1 -0
- package/dist/navigation.d.ts +1 -0
- package/dist/navigation.js +8 -9
- package/dist/peek.d.ts +1 -0
- package/dist/peek.js +6 -7
- package/dist/project-language.d.ts +2 -0
- package/dist/project-language.js +3 -0
- package/dist/rename.d.ts +1 -0
- package/dist/rename.js +10 -11
- package/dist/semantic-tokens.js +3 -5
- package/dist/symbol-picker.js +2 -3
- package/dist/symbols.js +3 -5
- package/dist/workspace-edit.d.ts +1 -0
- package/dist/workspace-edit.js +46 -7
- package/package.json +5 -5
- package/src/EditorView.tsx +5 -3
- package/src/HoverCard.tsx +44 -25
- package/src/LanguagePopups.tsx +8 -4
- package/src/code-actions-model.ts +1 -1
- package/src/code-actions.ts +20 -23
- package/src/code-authors.ts +3 -3
- package/src/code-vision-model.ts +4 -4
- package/src/code-vision.ts +12 -10
- package/src/completion.ts +1 -2
- package/src/editor-language.ts +26 -0
- package/src/folding.ts +14 -10
- package/src/history.ts +1 -2
- package/src/host-types.ts +3 -0
- package/src/hover.ts +1 -2
- package/src/inlay-hints.ts +3 -5
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +1 -0
- package/src/locales/nl.json +1 -0
- package/src/navigation.ts +9 -10
- package/src/peek.ts +7 -8
- package/src/project-language.ts +3 -0
- package/src/rename.ts +11 -12
- package/src/semantic-tokens.ts +3 -5
- package/src/symbol-picker.ts +2 -3
- package/src/symbols.ts +3 -5
- package/src/workspace-edit.ts +70 -10
package/dist/host-types.d.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/dist/inlay-hints.js
CHANGED
|
@@ -57,15 +57,13 @@ export class InlayHintsFeature {
|
|
|
57
57
|
this.refresher.later();
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
-
const providers =
|
|
61
|
-
|
|
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
|
|
66
|
+
providers();
|
|
69
67
|
this.refresher.dispose();
|
|
70
68
|
editor.setInlayHints([]);
|
|
71
69
|
});
|
package/dist/language-ids.js
CHANGED
|
@@ -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) {
|
package/dist/locales/en.json
CHANGED
package/dist/locales/nl.json
CHANGED
package/dist/navigation.d.ts
CHANGED
|
@@ -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;
|
package/dist/navigation.js
CHANGED
|
@@ -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
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;
|
package/dist/project-language.js
CHANGED
|
@@ -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
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
|
}
|
package/dist/semantic-tokens.js
CHANGED
|
@@ -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 =
|
|
29
|
-
|
|
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
|
|
33
|
+
providers();
|
|
36
34
|
this.refresher.dispose();
|
|
37
35
|
editor.setSemanticTokens(null);
|
|
38
36
|
});
|
package/dist/symbol-picker.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 =
|
|
79
|
-
|
|
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
|
|
83
|
+
providers();
|
|
86
84
|
this.refresher.dispose();
|
|
87
85
|
editor.setBlocks(null);
|
|
88
86
|
});
|
package/dist/workspace-edit.d.ts
CHANGED
|
@@ -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;
|
package/dist/workspace-edit.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
52
|
-
* edit is
|
|
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('
|
|
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
|
|
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
|
|
61
|
-
"@adecore/lsp": "0.17.0
|
|
62
|
-
"@adecore/merge": "0.17.0
|
|
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
|
|
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",
|
package/src/EditorView.tsx
CHANGED
|
@@ -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
|
}
|