@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.
- package/LICENSE +1 -1
- package/README.md +35 -30
- package/dist/EditorView.d.ts +3 -2
- package/dist/EditorView.js +2 -2
- package/dist/LanguagePopups.d.ts +3 -1
- package/dist/LanguagePopups.js +3 -3
- 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.js +3 -5
- 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/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/package.json +5 -5
- package/src/EditorView.tsx +5 -3
- package/src/LanguagePopups.tsx +8 -4
- 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 +3 -5
- 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/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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor-react",
|
|
3
|
-
"version": "0.17.0-beta.
|
|
3
|
+
"version": "0.17.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Language-enabled editors, feature coordination and React popups over injected language services.",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"test": "bun test --conditions=source"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@adecore/editor": "0.17.0-beta.
|
|
61
|
-
"@adecore/lsp": "0.17.0-beta.
|
|
62
|
-
"@adecore/merge": "0.17.0-beta.
|
|
60
|
+
"@adecore/editor": "0.17.0-beta.2",
|
|
61
|
+
"@adecore/lsp": "0.17.0-beta.2",
|
|
62
|
+
"@adecore/merge": "0.17.0-beta.2",
|
|
63
63
|
"zustand": "^5.0.15",
|
|
64
64
|
"lucide-react": "^1.47.0",
|
|
65
65
|
"react-markdown": "^10.1.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"remark-breaks": "^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@adecore/ui": "^0.17.0-beta.
|
|
70
|
+
"@adecore/ui": "^0.17.0-beta.2",
|
|
71
71
|
"react": "^19.3.0",
|
|
72
72
|
"react-dom": "^19.3.0",
|
|
73
73
|
"i18next": "^26.4.2",
|
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
|
}
|
package/src/LanguagePopups.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useReducer } from 'react';
|
|
1
|
+
import { useEffect, useReducer, type ReactNode } from 'react';
|
|
2
2
|
import { useStore } from 'zustand';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
@@ -15,9 +15,9 @@ import { SignatureCard } from './SignatureCard.tsx';
|
|
|
15
15
|
/*
|
|
16
16
|
* Every card a language feature opens over the editor. Each feature keeps its own state and these
|
|
17
17
|
* only draw it, placed by the screen position of the character it belongs to, which is read again
|
|
18
|
-
* whenever the editor scrolls or resizes.
|
|
18
|
+
* whenever the editor scrolls or resizes. `contextMenuItems` adds rows of the app to the context menu.
|
|
19
19
|
*/
|
|
20
|
-
export function LanguagePopups({ language }: { language: EditorLanguage }) {
|
|
20
|
+
export function LanguagePopups({ language, contextMenuItems }: { language: EditorLanguage; contextMenuItems?: (language: EditorLanguage) => ReactNode }) {
|
|
21
21
|
const [, redraw] = useReducer((count: number) => count + 1, 0);
|
|
22
22
|
const hover = useStore(language.popups, (state) => state.hover);
|
|
23
23
|
const completion = useStore(language.popups, (state) => state.completion);
|
|
@@ -51,7 +51,11 @@ export function LanguagePopups({ language }: { language: EditorLanguage }) {
|
|
|
51
51
|
)}
|
|
52
52
|
{completion !== null && completionRect !== null && <CompletionPopup language={language} view={completion} rect={completionRect} />}
|
|
53
53
|
{pick !== null && pickRect !== null && <PickPopup language={language} view={pick} rect={pickRect} />}
|
|
54
|
-
{menu !== null &&
|
|
54
|
+
{menu !== null && (
|
|
55
|
+
<EditorContextMenu language={language} view={menu}>
|
|
56
|
+
{contextMenuItems?.(language)}
|
|
57
|
+
</EditorContextMenu>
|
|
58
|
+
)}
|
|
55
59
|
{symbols !== null && <SymbolPicker language={language} view={symbols} />}
|
|
56
60
|
{peek !== null && <PeekPanel language={language} view={peek} />}
|
|
57
61
|
{authors !== null && <CodeAuthorsCard language={language} view={authors} />}
|
package/src/code-actions.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
1
|
import { ErrorCodes, LspError, StaleResultError, type CodeAction, type Command, type Diagnostic, type Position } from '@adecore/lsp';
|
|
3
2
|
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
4
3
|
import {
|
|
@@ -49,10 +48,6 @@ interface AskOptions {
|
|
|
49
48
|
readonly groups?: readonly ActionGroup[];
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
function say(key: string, options?: Record<string, unknown>): string {
|
|
53
|
-
return i18next.t(`editor:language.actions.${key}`, options);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
/*
|
|
57
52
|
* What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
|
|
58
53
|
* that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
|
|
@@ -72,12 +67,10 @@ export class CodeActionsFeature {
|
|
|
72
67
|
|
|
73
68
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
74
69
|
this.language = language;
|
|
75
|
-
const { editor
|
|
70
|
+
const { editor } = language;
|
|
76
71
|
this.refresher = new Refresher((signal) => this.hint(signal), PAUSE_MS, timers);
|
|
77
|
-
const providers =
|
|
78
|
-
|
|
79
|
-
this.refresher.later();
|
|
80
|
-
}
|
|
72
|
+
const providers = language.onProvidersChanged(() => {
|
|
73
|
+
this.refresher.later();
|
|
81
74
|
});
|
|
82
75
|
const offs = [
|
|
83
76
|
editor.onCaret(() => {
|
|
@@ -98,7 +91,7 @@ export class CodeActionsFeature {
|
|
|
98
91
|
return false;
|
|
99
92
|
}),
|
|
100
93
|
language.diagnostics.onChange(() => this.refresher.later()),
|
|
101
|
-
|
|
94
|
+
providers
|
|
102
95
|
];
|
|
103
96
|
language.onDispose(() => {
|
|
104
97
|
for (const off of offs) {
|
|
@@ -137,7 +130,7 @@ export class CodeActionsFeature {
|
|
|
137
130
|
const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
|
|
138
131
|
const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
|
|
139
132
|
if (entry === undefined) {
|
|
140
|
-
this.tell('success', say(entries === null ? 'unavailable' : 'organized'));
|
|
133
|
+
this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
|
|
141
134
|
return;
|
|
142
135
|
}
|
|
143
136
|
await this.apply(entry);
|
|
@@ -146,13 +139,13 @@ export class CodeActionsFeature {
|
|
|
146
139
|
async formatDocument(): Promise<void> {
|
|
147
140
|
const { editor, project, uri } = this.language;
|
|
148
141
|
if (!project.service.supports('textDocument/formatting', uri)) {
|
|
149
|
-
this.tell('error', say('noFormatter'));
|
|
142
|
+
this.tell('error', this.say('noFormatter'));
|
|
150
143
|
return;
|
|
151
144
|
}
|
|
152
145
|
try {
|
|
153
146
|
const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
|
|
154
147
|
if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
|
|
155
|
-
this.tell('error', say('readOnly'));
|
|
148
|
+
this.tell('error', this.say('readOnly'));
|
|
156
149
|
}
|
|
157
150
|
} catch (error) {
|
|
158
151
|
this.fail(error);
|
|
@@ -175,7 +168,7 @@ export class CodeActionsFeature {
|
|
|
175
168
|
|
|
176
169
|
private async ask(options: AskOptions): Promise<void> {
|
|
177
170
|
if (!this.supported) {
|
|
178
|
-
this.tell('error', say('unavailable'));
|
|
171
|
+
this.tell('error', this.say('unavailable'));
|
|
179
172
|
return;
|
|
180
173
|
}
|
|
181
174
|
// The press on a card or the gutter may have taken the focus, and the list answers to the editor's keys.
|
|
@@ -193,13 +186,13 @@ export class CodeActionsFeature {
|
|
|
193
186
|
return;
|
|
194
187
|
}
|
|
195
188
|
if (entries.length === 0) {
|
|
196
|
-
this.tell('success', say('none'));
|
|
189
|
+
this.tell('success', this.say('none'));
|
|
197
190
|
return;
|
|
198
191
|
}
|
|
199
192
|
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
200
193
|
const groups: PickGroup[] = (options.groups ?? ACTION_GROUPS).flatMap((group): PickGroup[] => {
|
|
201
194
|
const rows = entries.filter((entry) => entry.group === group).map((entry) => ({ id: entry.id, label: entry.action.title, detail: '' }));
|
|
202
|
-
return rows.length === 0 ? [] : [{ title: say(`groups.${group}`), rows }];
|
|
195
|
+
return rows.length === 0 ? [] : [{ title: this.say(`groups.${group}`), rows }];
|
|
203
196
|
});
|
|
204
197
|
this.language.completion.close();
|
|
205
198
|
this.language.pick.open({
|
|
@@ -278,7 +271,7 @@ export class CodeActionsFeature {
|
|
|
278
271
|
if (signal.aborted || this.asking) {
|
|
279
272
|
return;
|
|
280
273
|
}
|
|
281
|
-
editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: say('show') } : null);
|
|
274
|
+
editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: this.say('show') } : null);
|
|
282
275
|
}
|
|
283
276
|
|
|
284
277
|
private async resolve(entry: ActionEntry, signal?: AbortSignal): Promise<CodeAction> {
|
|
@@ -325,9 +318,9 @@ export class CodeActionsFeature {
|
|
|
325
318
|
return null;
|
|
326
319
|
}
|
|
327
320
|
const notes = [
|
|
328
|
-
...(found.hiddenLines > 0 ? [say('moreLines', { count: found.hiddenLines })] : []),
|
|
329
|
-
...(found.otherFiles > 0 ? [say('otherFiles', { count: found.otherFiles })] : []),
|
|
330
|
-
...(found.moves > 0 ? [say('moves', { count: found.moves })] : [])
|
|
321
|
+
...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
|
|
322
|
+
...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
|
|
323
|
+
...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
|
|
331
324
|
];
|
|
332
325
|
return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
|
|
333
326
|
}
|
|
@@ -353,7 +346,7 @@ export class CodeActionsFeature {
|
|
|
353
346
|
if (action.edit !== undefined) {
|
|
354
347
|
const result = await project.applyWorkspaceEdit(action.edit);
|
|
355
348
|
if (!result.applied) {
|
|
356
|
-
this.tell('error', say('failed', { message: result.failureReason ?? say('refused') }));
|
|
349
|
+
this.tell('error', this.say('failed', { message: result.failureReason ?? this.say('refused') }));
|
|
357
350
|
return;
|
|
358
351
|
}
|
|
359
352
|
}
|
|
@@ -381,7 +374,11 @@ export class CodeActionsFeature {
|
|
|
381
374
|
|
|
382
375
|
private fail(error: unknown): void {
|
|
383
376
|
if (!(error instanceof StaleResultError)) {
|
|
384
|
-
this.tell('error', say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
377
|
+
this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
385
378
|
}
|
|
386
379
|
}
|
|
380
|
+
|
|
381
|
+
private say(key: string, options?: Record<string, unknown>): string {
|
|
382
|
+
return this.language.project.i18n.t(`editor:language.actions.${key}`, options);
|
|
383
|
+
}
|
|
387
384
|
}
|
package/src/code-authors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { i18n } from 'i18next';
|
|
2
2
|
import { diffLines, splitLines } from '@adecore/merge';
|
|
3
3
|
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
4
4
|
import { formatNumber } from '@adecore/ui/format';
|
|
@@ -94,10 +94,10 @@ export function authorshipOf(commits: readonly GitBlameCommit[], mapped: Int32Ar
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/* The row's words: the author with most lines, how many others wrote some, and a star when the range is edited since the commit. */
|
|
97
|
-
export function authorsText(authorship: CodeAuthorship): string {
|
|
97
|
+
export function authorsText(authorship: CodeAuthorship, i18n: i18n): string {
|
|
98
98
|
const main = authorship.authors[0];
|
|
99
99
|
if (main === undefined) {
|
|
100
|
-
return
|
|
100
|
+
return i18n.t('editor:language.codeVision.newCode');
|
|
101
101
|
}
|
|
102
102
|
const others = authorship.authors.length - 1;
|
|
103
103
|
return `${main.name}${others > 0 ? ` +${formatNumber(others)}` : ''}${authorship.modified ? ' *' : ''}`;
|
package/src/code-vision-model.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DocumentSymbol, DocumentSymbolResult, Position, SymbolInformation } from '@adecore/lsp';
|
|
2
2
|
import { formatNumber } from '@adecore/ui/format';
|
|
3
|
-
import
|
|
3
|
+
import type { i18n } from 'i18next';
|
|
4
4
|
|
|
5
5
|
/* More declarations than this in one file and the rows are left out, which is the file that is too big for them to be quiet. */
|
|
6
6
|
export const MAX_DECLARATIONS = 3000;
|
|
@@ -223,8 +223,8 @@ export function declarationsOf(result: DocumentSymbolResult, source: Declaration
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/* "No usages", "1 usage" and "N usages". */
|
|
226
|
-
export function usagesText(count: number): string {
|
|
226
|
+
export function usagesText(count: number, i18n: i18n): string {
|
|
227
227
|
return count === 0
|
|
228
|
-
?
|
|
229
|
-
:
|
|
228
|
+
? i18n.t('editor:language.codeVision.usagesNone')
|
|
229
|
+
: i18n.t('editor:language.codeVision.usages', { count, formatted: formatNumber(count) });
|
|
230
230
|
}
|
package/src/code-vision.ts
CHANGED
|
@@ -62,7 +62,7 @@ export class CodeVisionFeature {
|
|
|
62
62
|
constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
|
|
63
63
|
this.language = language;
|
|
64
64
|
this.timers = timers;
|
|
65
|
-
const { editor
|
|
65
|
+
const { editor } = language;
|
|
66
66
|
const offs = [
|
|
67
67
|
onSymbols((result) => this.read(result)),
|
|
68
68
|
editor.onTextChange(() => {
|
|
@@ -74,19 +74,17 @@ export class CodeVisionFeature {
|
|
|
74
74
|
this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
|
|
75
75
|
})
|
|
76
76
|
];
|
|
77
|
-
const providers =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.plan();
|
|
82
|
-
}
|
|
77
|
+
const providers = language.onProvidersChanged(() => {
|
|
78
|
+
this.failed.clear();
|
|
79
|
+
this.publish();
|
|
80
|
+
this.plan();
|
|
83
81
|
});
|
|
84
82
|
language.onDispose(() => {
|
|
85
83
|
this.disposed = true;
|
|
86
84
|
for (const off of offs) {
|
|
87
85
|
off();
|
|
88
86
|
}
|
|
89
|
-
providers
|
|
87
|
+
providers();
|
|
90
88
|
this.timers.clear(this.scrollTimer);
|
|
91
89
|
for (const controller of this.running.values()) {
|
|
92
90
|
controller.abort();
|
|
@@ -209,7 +207,11 @@ export class CodeVisionFeature {
|
|
|
209
207
|
const entries: EditorCodeVisionEntry[] = [];
|
|
210
208
|
const known = this.counts.get(declaration.id);
|
|
211
209
|
if (counted && declaration.usages && known !== undefined) {
|
|
212
|
-
entries.push({
|
|
210
|
+
entries.push({
|
|
211
|
+
id: 'usages',
|
|
212
|
+
text: usagesText(known.count, this.language.project.i18n),
|
|
213
|
+
activate: () => void this.language.peek.open(declaration.position)
|
|
214
|
+
});
|
|
213
215
|
}
|
|
214
216
|
if (mapped !== null && declaration.authors) {
|
|
215
217
|
let authorship = this.authorships.get(declaration.id);
|
|
@@ -220,7 +222,7 @@ export class CodeVisionFeature {
|
|
|
220
222
|
const shown = authorship;
|
|
221
223
|
entries.push({
|
|
222
224
|
id: 'authors',
|
|
223
|
-
text: authorsText(shown),
|
|
225
|
+
text: authorsText(shown, this.language.project.i18n),
|
|
224
226
|
...(shown.authors.length > 0 ? { icon: shown.authors.length > 1 ? ('users' as const) : ('user' as const) } : {}),
|
|
225
227
|
activate: (anchor) => this.showAuthors(declaration, shown, anchor)
|
|
226
228
|
});
|
package/src/completion.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
1
|
import { StaleResultError, type CompletionContext, type CompletionItem } from '@adecore/lsp';
|
|
3
2
|
import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
|
|
4
3
|
import { comparePositions, shiftPosition } from './diagnostics-model.ts';
|
|
@@ -399,7 +398,7 @@ export class CompletionFeature {
|
|
|
399
398
|
const asked = this.explicit && !refresh;
|
|
400
399
|
this.refilter();
|
|
401
400
|
if (asked && !this.isOpen && !this.incomplete) {
|
|
402
|
-
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title:
|
|
401
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
|
|
403
402
|
} else if (asked && this.ranked.length === 1 && !this.incomplete) {
|
|
404
403
|
// One answer to a list that was asked for is the one wanted.
|
|
405
404
|
void this.accept(false, 0);
|