@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/src/HoverCard.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
2
|
import { CircleX, Info, TriangleAlert } from 'lucide-react';
|
|
3
|
-
import { fileUriToPath } from '@adecore/lsp';
|
|
3
|
+
import { fileUriToPath, type Location } from '@adecore/lsp';
|
|
4
4
|
import { Button, Icon, Tooltip } from '@adecore/ui';
|
|
5
5
|
import { formatNumber } from '@adecore/ui/format';
|
|
6
6
|
import type { EditorPosition } from '@adecore/editor';
|
|
@@ -13,6 +13,28 @@ import { SymbolSections } from './HoverSections.tsx';
|
|
|
13
13
|
const SEVERITY_ICONS = { error: CircleX, warning: TriangleAlert, info: Info, hint: Info } as const;
|
|
14
14
|
const SEVERITY_COLORS = { error: 'text-status-error', warning: 'text-status-needs-you', info: 'text-status-running', hint: 'text-text-muted' } as const;
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
* Where a definition is: only its line in the file of the card, else the file and the line, with a long name cut
|
|
18
|
+
* short before the line. A definition outside the project folder, such as the stubs of a standard library, has none.
|
|
19
|
+
*/
|
|
20
|
+
export function DefinitionPlace({ definition, uri, folder }: { definition: Location; uri: string; folder: string }) {
|
|
21
|
+
const { t } = useTranslation('editor');
|
|
22
|
+
const path = fileUriToPath(definition.uri);
|
|
23
|
+
const line = formatNumber(definition.range.start.line + 1);
|
|
24
|
+
if (path === null || (folder !== '' && !path.startsWith(`${folder}/`))) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (definition.uri === uri) {
|
|
28
|
+
return <span className="text-text-faint">{t('language.hover.line', { line })}</span>;
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<span className="flex min-w-0 font-mono text-text-faint">
|
|
32
|
+
<span className="truncate">{basenameOf(path)}</span>
|
|
33
|
+
<span className="shrink-0">:{line}</span>
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
function ProblemSection({ problem, language }: { problem: Problem; language: EditorLanguage }) {
|
|
17
39
|
const { t } = useTranslation('editor');
|
|
18
40
|
const { diagnostic } = problem;
|
|
@@ -75,7 +97,6 @@ export function HoverCard({
|
|
|
75
97
|
position: EditorPosition;
|
|
76
98
|
}) {
|
|
77
99
|
const { t } = useTranslation('editor');
|
|
78
|
-
const place = info?.definition === null || info === null ? null : fileUriToPath(info.definition.uri);
|
|
79
100
|
return (
|
|
80
101
|
<div className="flex w-max min-w-[280px] max-w-[min(520px,calc(100vw-16px))] flex-col divide-y divide-border">
|
|
81
102
|
{info !== null && (
|
|
@@ -87,29 +108,27 @@ export function HoverCard({
|
|
|
87
108
|
void language.navigation.goToName(name, anchor, name === (declared ?? info.word) ? info.definition : null);
|
|
88
109
|
}}
|
|
89
110
|
/>
|
|
90
|
-
<div className="flex
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</span>
|
|
112
|
-
)}
|
|
111
|
+
<div className="flex flex-col gap-0.5 px-3 py-1.5 text-xs">
|
|
112
|
+
<div className="flex items-center gap-3 whitespace-nowrap">
|
|
113
|
+
{info.definition !== null && (
|
|
114
|
+
<button type="button" className="text-accent hover:underline" onClick={() => language.goTo(info.definition!)}>
|
|
115
|
+
{t('language.hover.definition')}
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
{info.references !== null && info.references > 0 && (
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
className="text-accent hover:underline"
|
|
122
|
+
onClick={() => {
|
|
123
|
+
language.hover.hide();
|
|
124
|
+
void language.peek.open(position);
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{t('language.hover.references', { count: info.references, formatted: formatNumber(info.references) })}
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
{info.definition !== null && <DefinitionPlace definition={info.definition} uri={language.uri} folder={language.project.folder} />}
|
|
113
132
|
</div>
|
|
114
133
|
</>
|
|
115
134
|
)}
|
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} />}
|
|
@@ -107,7 +107,7 @@ function linesOf(text: string): string[] {
|
|
|
107
107
|
return text.split(/\r\n|\r|\n/);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/* Null when the edit changes nothing, or
|
|
110
|
+
/* Null when the edit changes nothing, or deletes a file, which a preview of lines cannot show. A file it creates counts as another file. */
|
|
111
111
|
export function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditPreview | null {
|
|
112
112
|
const entries = entriesOf(edit);
|
|
113
113
|
if (entries === null) {
|
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);
|
package/src/editor-language.ts
CHANGED
|
@@ -145,6 +145,32 @@ export class EditorLanguage {
|
|
|
145
145
|
this.pick.open({ anchor, title, groups: [{ title: null, rows }], accept: (id) => this.goTo(places[Number(id)]!) });
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/*
|
|
149
|
+
* Runs `listener` once the document is open and again whenever the service says what it can ask for
|
|
150
|
+
* this document changed. A service that is ready before the document opens never says so, so the
|
|
151
|
+
* opening counts as the first change.
|
|
152
|
+
*/
|
|
153
|
+
onProvidersChanged(listener: () => void): () => void {
|
|
154
|
+
let listening = true;
|
|
155
|
+
void this.document.ready.then(
|
|
156
|
+
() => {
|
|
157
|
+
if (listening && !this.disposed) {
|
|
158
|
+
listener();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
() => undefined
|
|
162
|
+
);
|
|
163
|
+
const providers = this.project.service.onProvidersChanged((uri) => {
|
|
164
|
+
if (uri === this.uri) {
|
|
165
|
+
listener();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
listening = false;
|
|
170
|
+
providers.dispose();
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
148
174
|
/* Runs `dispose` when the editor lets go of its document. */
|
|
149
175
|
onDispose(dispose: () => void): void {
|
|
150
176
|
this.disposers.push(dispose);
|
package/src/folding.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditorFoldHints,
|
|
1
|
+
import type { EditorFoldHints, EditorFoldRangeHint, EditorFoldSymbol } from '@adecore/editor';
|
|
2
2
|
import type { DocumentSymbol, DocumentSymbolResult, FoldingRange, SymbolInformation } from '@adecore/lsp';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { Refresher } from './refresher.ts';
|
|
@@ -51,11 +51,17 @@ export function foldSymbolsOf(result: DocumentSymbolResult): EditorFoldSymbol[]
|
|
|
51
51
|
return found;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/* The ranges a server folds that span more than one line. */
|
|
55
|
-
export function foldRangesOf(ranges: readonly FoldingRange[] | null):
|
|
54
|
+
/* The ranges a server folds that span more than one line, with where a range starts and ends within its lines when it says. */
|
|
55
|
+
export function foldRangesOf(ranges: readonly FoldingRange[] | null): EditorFoldRangeHint[] {
|
|
56
56
|
return (ranges ?? [])
|
|
57
57
|
.filter((range) => range.endLine > range.startLine)
|
|
58
|
-
.map((range) => ({
|
|
58
|
+
.map((range) => ({
|
|
59
|
+
startLine: range.startLine,
|
|
60
|
+
endLine: range.endLine,
|
|
61
|
+
...(range.kind === undefined ? {} : { kind: range.kind }),
|
|
62
|
+
...(range.startCharacter === undefined ? {} : { startCharacter: range.startCharacter }),
|
|
63
|
+
...(range.endCharacter === undefined ? {} : { endCharacter: range.endCharacter })
|
|
64
|
+
}));
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/*
|
|
@@ -65,7 +71,7 @@ export function foldRangesOf(ranges: readonly FoldingRange[] | null): (EditorFol
|
|
|
65
71
|
*/
|
|
66
72
|
export class FoldingFeature {
|
|
67
73
|
private symbols: EditorFoldSymbol[] = [];
|
|
68
|
-
private ranges:
|
|
74
|
+
private ranges: EditorFoldRangeHint[] = [];
|
|
69
75
|
private readonly refresher: Refresher;
|
|
70
76
|
private readonly language: EditorLanguage;
|
|
71
77
|
|
|
@@ -91,15 +97,13 @@ export class FoldingFeature {
|
|
|
91
97
|
this.publish();
|
|
92
98
|
});
|
|
93
99
|
const edits = editor.onTextChange(() => this.refresher.later());
|
|
94
|
-
const providers =
|
|
95
|
-
|
|
96
|
-
this.refresher.now();
|
|
97
|
-
}
|
|
100
|
+
const providers = language.onProvidersChanged(() => {
|
|
101
|
+
this.refresher.now();
|
|
98
102
|
});
|
|
99
103
|
language.onDispose(() => {
|
|
100
104
|
symbols();
|
|
101
105
|
edits();
|
|
102
|
-
providers
|
|
106
|
+
providers();
|
|
103
107
|
this.refresher.dispose();
|
|
104
108
|
editor.setFoldHints(null);
|
|
105
109
|
});
|
package/src/history.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
1
|
import type { EditorLanguage } from './editor-language.ts';
|
|
3
2
|
import { isShortcut } from './shortcut-keys.ts';
|
|
4
3
|
|
|
@@ -51,7 +50,7 @@ export class HistoryFeature {
|
|
|
51
50
|
this.language.locations(
|
|
52
51
|
places.map((place) => ({ uri: place.uri, range: { start: place.position, end: place.position } })),
|
|
53
52
|
this.language.editor.getCaret(),
|
|
54
|
-
|
|
53
|
+
this.language.project.i18n.t('editor:language.history.recent')
|
|
55
54
|
);
|
|
56
55
|
}
|
|
57
56
|
}
|
package/src/host-types.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';
|
|
@@ -37,6 +38,8 @@ export interface RenameSuggestionsRequest {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export interface LanguageHost {
|
|
41
|
+
/* The instance the app hands `UIProvider`, which the features read their words from too; the default instance without one. */
|
|
42
|
+
readonly i18n?: i18n;
|
|
40
43
|
readonly folder?: string;
|
|
41
44
|
readonly files?: ProjectFiles;
|
|
42
45
|
readonly apple?: boolean;
|
package/src/hover.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
1
|
import type { EditorHover, EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
2
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
3
|
import { rangeHolds, type Problem } from './diagnostics-model.ts';
|
|
@@ -212,7 +211,7 @@ export class HoverFeature {
|
|
|
212
211
|
if (problems.length === 0 && info === null) {
|
|
213
212
|
this.hide();
|
|
214
213
|
if (pinned) {
|
|
215
|
-
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title:
|
|
214
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.hover.none') });
|
|
216
215
|
}
|
|
217
216
|
return;
|
|
218
217
|
}
|
package/src/inlay-hints.ts
CHANGED
|
@@ -71,15 +71,13 @@ export class InlayHintsFeature {
|
|
|
71
71
|
this.refresher.later();
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
const providers =
|
|
75
|
-
|
|
76
|
-
this.refresher.now();
|
|
77
|
-
}
|
|
74
|
+
const providers = language.onProvidersChanged(() => {
|
|
75
|
+
this.refresher.now();
|
|
78
76
|
});
|
|
79
77
|
language.onDispose(() => {
|
|
80
78
|
edits();
|
|
81
79
|
scrolls();
|
|
82
|
-
providers
|
|
80
|
+
providers();
|
|
83
81
|
this.refresher.dispose();
|
|
84
82
|
editor.setInlayHints([]);
|
|
85
83
|
});
|
package/src/language-ids.ts
CHANGED
|
@@ -15,7 +15,8 @@ const LSP_IDS: Record<string, string> = {
|
|
|
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
|
|
|
21
22
|
/* The LSP language id of a file, or null when no language server here knows its language. */
|
|
@@ -45,7 +46,8 @@ const PATH_IDS: Record<string, string> = {
|
|
|
45
46
|
css: 'css',
|
|
46
47
|
scss: 'scss',
|
|
47
48
|
html: 'html',
|
|
48
|
-
md: 'markdown'
|
|
49
|
+
md: 'markdown',
|
|
50
|
+
twig: 'twig'
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/* The highlighter id for the code of a file we only know by its path, such as a place another file refers to. */
|
package/src/locales/en.json
CHANGED
package/src/locales/nl.json
CHANGED
package/src/navigation.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import i18next from 'i18next';
|
|
2
1
|
import { StaleResultError, fileUriToPath, type Location, type LocationLink, type NavigationResult, type Range } from '@adecore/lsp';
|
|
3
2
|
import type { EditorPosition } from '@adecore/editor';
|
|
4
3
|
import { basenameOf } from './paths.ts';
|
|
@@ -24,10 +23,6 @@ const SHORTCUT_KINDS: readonly [NavigationKind, 'goToDefinition' | 'goToTypeDefi
|
|
|
24
23
|
|
|
25
24
|
const TOAST_ID = 'language-navigation';
|
|
26
25
|
|
|
27
|
-
function say(key: string, options?: Record<string, unknown>): string {
|
|
28
|
-
return i18next.t(`editor:language.navigation.${key}`, options);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
/* 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. */
|
|
32
27
|
export function uniquePlaces(result: NavigationResult): { location: Location; target: Range | null }[] {
|
|
33
28
|
const seen = new Set<string>();
|
|
@@ -96,7 +91,7 @@ export class NavigationFeature {
|
|
|
96
91
|
async go(kind: NavigationKind, position: EditorPosition = this.language.editor.getCaret()): Promise<void> {
|
|
97
92
|
const { project, uri, editor } = this.language;
|
|
98
93
|
if (!this.supports(kind)) {
|
|
99
|
-
this.tell(say('unavailable'));
|
|
94
|
+
this.tell(this.say('unavailable'));
|
|
100
95
|
return;
|
|
101
96
|
}
|
|
102
97
|
let result: NavigationResult;
|
|
@@ -111,20 +106,20 @@ export class NavigationFeature {
|
|
|
111
106
|
: service.implementation(uri, position));
|
|
112
107
|
} catch (error) {
|
|
113
108
|
if (!(error instanceof StaleResultError)) {
|
|
114
|
-
this.tell(say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
109
|
+
this.tell(this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
115
110
|
}
|
|
116
111
|
return;
|
|
117
112
|
}
|
|
118
113
|
const places = uniqueLocations(result);
|
|
119
114
|
if (places.length === 0) {
|
|
120
|
-
this.tell(say('none'));
|
|
115
|
+
this.tell(this.say('none'));
|
|
121
116
|
} else if (kind === 'definition' && places.length === 1 && places[0]!.uri === uri && rangeHolds(places[0]!.range, position)) {
|
|
122
117
|
// The name is its own definition, so what a person wants to see is where it is used.
|
|
123
118
|
void this.language.peek.open(position);
|
|
124
119
|
} else if (places.length === 1) {
|
|
125
120
|
this.language.goTo(places[0]!);
|
|
126
121
|
} else {
|
|
127
|
-
this.language.locations(places, position, say(`titles.${kind}`));
|
|
122
|
+
this.language.locations(places, position, this.say(`titles.${kind}`));
|
|
128
123
|
}
|
|
129
124
|
editor.focus();
|
|
130
125
|
}
|
|
@@ -148,7 +143,7 @@ export class NavigationFeature {
|
|
|
148
143
|
if (places.length === 1) {
|
|
149
144
|
this.language.goTo(places[0]!);
|
|
150
145
|
} else if (places.length > 1) {
|
|
151
|
-
this.language.locations(places, anchor, say('titles.symbol'));
|
|
146
|
+
this.language.locations(places, anchor, this.say('titles.symbol'));
|
|
152
147
|
}
|
|
153
148
|
} catch {
|
|
154
149
|
// A name that cannot be found is a link that does nothing, not an error.
|
|
@@ -158,6 +153,10 @@ export class NavigationFeature {
|
|
|
158
153
|
private tell(title: string): void {
|
|
159
154
|
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
|
|
160
155
|
}
|
|
156
|
+
|
|
157
|
+
private say(key: string, options?: Record<string, unknown>): string {
|
|
158
|
+
return this.language.project.i18n.t(`editor:language.navigation.${key}`, options);
|
|
159
|
+
}
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
/* How a place reads in a list: the file and the line, with the folder it is in. */
|