@adecore/editor-react 0.21.1 → 0.22.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/dist/AnchoredPopup.js +2 -3
- package/dist/HoverSections.js +5 -11
- package/dist/PeekPanel.js +6 -13
- package/dist/RenameCard.js +2 -2
- package/dist/SymbolPicker.js +2 -3
- package/dist/code-actions-model.js +9 -8
- package/dist/code-actions.d.ts +1 -1
- package/dist/code-actions.js +21 -20
- package/dist/code-authors.js +1 -1
- package/dist/code-vision.d.ts +1 -0
- package/dist/code-vision.js +17 -14
- package/dist/completion-call.js +2 -6
- package/dist/completion-model.d.ts +3 -2
- package/dist/completion-model.js +8 -9
- package/dist/completion.js +8 -10
- package/dist/context-menu.js +4 -5
- package/dist/definition-link.js +2 -1
- package/dist/diagnostics-model.d.ts +2 -0
- package/dist/diagnostics-model.js +8 -2
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +13 -12
- package/dist/editor-language.d.ts +2 -2
- package/dist/editor-language.js +10 -10
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +3 -0
- package/dist/folding.js +5 -24
- package/dist/history.d.ts +1 -0
- package/dist/history.js +9 -12
- package/dist/hover-content.d.ts +1 -0
- package/dist/hover-content.js +5 -4
- package/dist/hover.d.ts +2 -2
- package/dist/hover.js +10 -9
- package/dist/inlay-hints.js +4 -4
- package/dist/language-ids.js +1 -1
- package/dist/line-text.d.ts +3 -0
- package/dist/line-text.js +7 -0
- package/dist/navigation.js +9 -9
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +4 -0
- package/dist/peek.js +11 -4
- package/dist/project-language.js +15 -13
- package/dist/project-problems.d.ts +1 -2
- package/dist/project-problems.js +2 -4
- package/dist/refresher.js +11 -6
- package/dist/rename.d.ts +2 -2
- package/dist/rename.js +14 -12
- package/dist/review-group.d.ts +1 -0
- package/dist/review-group.js +10 -14
- package/dist/row-host.d.ts +13 -20
- package/dist/row-host.js +33 -60
- package/dist/semantic-model.js +4 -6
- package/dist/semantic-tokens.js +2 -4
- package/dist/signature.js +1 -1
- package/dist/snippet-session.js +3 -4
- package/dist/symbol-links.js +2 -2
- package/dist/symbol-picker-model.js +15 -9
- package/dist/symbol-picker.js +2 -1
- package/dist/symbols.d.ts +7 -1
- package/dist/symbols.js +22 -28
- package/dist/testing.js +3 -3
- package/dist/word-diff.js +2 -4
- package/dist/workspace-edit.js +7 -8
- package/package.json +5 -5
- package/src/AnchoredPopup.tsx +2 -3
- package/src/HoverSections.tsx +5 -11
- package/src/PeekPanel.tsx +6 -14
- package/src/RenameCard.tsx +2 -2
- package/src/SymbolPicker.tsx +3 -4
- package/src/code-actions-model.ts +10 -8
- package/src/code-actions.ts +36 -38
- package/src/code-authors.ts +2 -2
- package/src/code-vision.ts +19 -15
- package/src/completion-call.ts +2 -6
- package/src/completion-model.ts +10 -12
- package/src/completion.ts +8 -13
- package/src/context-menu.ts +4 -5
- package/src/definition-link.ts +2 -1
- package/src/diagnostics-model.ts +10 -2
- package/src/diagnostics.ts +15 -13
- package/src/editor-language.ts +11 -11
- package/src/error-message.ts +3 -0
- package/src/folding.ts +6 -26
- package/src/history.ts +11 -12
- package/src/hover-content.ts +6 -4
- package/src/hover.ts +10 -9
- package/src/inlay-hints.ts +4 -4
- package/src/language-ids.ts +1 -1
- package/src/line-text.ts +10 -0
- package/src/navigation.ts +11 -10
- package/src/paths.ts +5 -0
- package/src/peek.ts +11 -10
- package/src/project-language.ts +17 -16
- package/src/project-problems.ts +3 -6
- package/src/refresher.ts +12 -6
- package/src/rename.ts +16 -18
- package/src/review-group.ts +11 -15
- package/src/row-host.ts +54 -76
- package/src/semantic-model.ts +4 -6
- package/src/semantic-tokens.ts +2 -4
- package/src/signature.ts +1 -1
- package/src/snippet-session.ts +3 -4
- package/src/symbol-links.ts +2 -2
- package/src/symbol-picker-model.ts +17 -9
- package/src/symbol-picker.ts +2 -1
- package/src/symbols.ts +29 -28
- package/src/testing.ts +4 -2
- package/src/view-state.ts +2 -0
- package/src/word-diff.ts +2 -4
- package/src/workspace-edit.ts +7 -8
package/src/HoverSections.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useEditorRendering } from './rendering-context.ts';
|
|
2
1
|
import { useEffect, useState, type MouseEvent } from 'react';
|
|
3
|
-
import { Markdown } from './Markdown.tsx';
|
|
4
2
|
import { Tooltip } from '@adecore/ui';
|
|
5
|
-
import { hoverSectionsOf, markdownParts, type DocTag, type HoverSection, type HoverText } from './hover-content.ts';
|
|
6
3
|
import { BaselineStatus } from './BaselineStatus.tsx';
|
|
4
|
+
import { hoverSectionsOf, markdownParts, type DocTag, type HoverSection, type HoverText } from './hover-content.ts';
|
|
5
|
+
import { Markdown } from './Markdown.tsx';
|
|
6
|
+
import { useEditorRendering } from './rendering-context.ts';
|
|
7
7
|
import { declaredNameOf, linkTypeNames } from './symbol-links.ts';
|
|
8
8
|
|
|
9
9
|
/* Code as the viewer colors it, plain until the grammar is in so the card never changes size under the pointer by much. */
|
|
@@ -102,10 +102,7 @@ function phpWithoutOpenTag(html: string): string {
|
|
|
102
102
|
return html.replace(/<span[^>]*><\?<\/span><span[^>]*>php<\/span>(<span[^>]*>) /, '$1');
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/*
|
|
106
|
-
* Where a suggestion comes from, such as `use Raxos\Database\Orm\Attribute\PrimaryKey`, on one line that gives
|
|
107
|
-
* way from its start, so the end of the name stays readable and a word is never cut in two. The full text is in the tooltip.
|
|
108
|
-
*/
|
|
105
|
+
/* Where a suggestion comes from, such as a PHP `use` line, cut from its start so the end of the name stays readable. */
|
|
109
106
|
export function SourceLine({ text, language }: { text: string; language: string }) {
|
|
110
107
|
const { theme, highlight } = useEditorRendering();
|
|
111
108
|
const [html, setHtml] = useState<{ key: string; html: string } | null>(null);
|
|
@@ -113,10 +110,7 @@ export function SourceLine({ text, language }: { text: string; language: string
|
|
|
113
110
|
const key = `${theme}\0${text}`;
|
|
114
111
|
|
|
115
112
|
useEffect(() => {
|
|
116
|
-
if (!highlighted) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (highlight === undefined) {
|
|
113
|
+
if (!highlighted || highlight === undefined) {
|
|
120
114
|
return;
|
|
121
115
|
}
|
|
122
116
|
let alive = true;
|
package/src/PeekPanel.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useEditorRendering } from './rendering-context.ts';
|
|
2
1
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
2
|
import { createPortal } from 'react-dom';
|
|
4
3
|
import { useTranslation } from 'react-i18next';
|
|
@@ -6,13 +5,14 @@ import { ChevronDown, ChevronRight, X } from 'lucide-react';
|
|
|
6
5
|
import { fileUriToPath } from '@adecore/lsp';
|
|
7
6
|
import { FileIcon, IconButton, Tooltip } from '@adecore/ui';
|
|
8
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
9
|
-
import { basenameOf } from './paths.ts';
|
|
8
|
+
import { basenameOf, relativeTo } from './paths.ts';
|
|
10
9
|
import type { EditorLanguage } from './editor-language.ts';
|
|
11
10
|
import { shikiLanguageOfPath } from './language-ids.ts';
|
|
12
11
|
import { PathText } from './PathText.tsx';
|
|
13
12
|
import { PEEK_HEIGHT } from './peek.ts';
|
|
14
13
|
import { distinguishingFolders, visualColumnOf, type NameRange, type PeekPlace } from './peek-model.ts';
|
|
15
14
|
import type { PeekView } from './popups.ts';
|
|
15
|
+
import { useEditorRendering } from './rendering-context.ts';
|
|
16
16
|
|
|
17
17
|
const TAB_SIZE = 4;
|
|
18
18
|
const CODE =
|
|
@@ -28,10 +28,7 @@ function Preview({ view }: { view: PeekView }) {
|
|
|
28
28
|
const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
|
|
29
29
|
|
|
30
30
|
useEffect(() => {
|
|
31
|
-
if (preview === null) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (highlight === undefined) {
|
|
31
|
+
if (preview === null || highlight === undefined) {
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
37
34
|
let alive = true;
|
|
@@ -98,10 +95,6 @@ function columnsOf(line: string, name: NameRange): NameRange {
|
|
|
98
95
|
return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
|
|
99
96
|
}
|
|
100
97
|
|
|
101
|
-
function relativeTo(path: string, folder: string): string {
|
|
102
|
-
return path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
98
|
function folderOf(path: string): string {
|
|
106
99
|
return path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
107
100
|
}
|
|
@@ -177,10 +170,9 @@ function Places({ language, view }: { language: EditorLanguage; view: PeekView }
|
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
/*
|
|
180
|
-
* The references of a name
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* the editor's own ground and no edge of its own, so it reads as part of the file.
|
|
173
|
+
* The references of a name in the row the editor leaves under the line: the code around the active one on
|
|
174
|
+
* the left and every place on the right. It portals into that row, which the editor makes again when it
|
|
175
|
+
* scrolls back into view, and has no edge of its own, so it reads as part of the file.
|
|
184
176
|
*/
|
|
185
177
|
export function PeekPanel({ language, view }: { language: EditorLanguage; view: PeekView }) {
|
|
186
178
|
const { t } = useTranslation('editor');
|
package/src/RenameCard.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { fileUriToPath } from '@adecore/lsp';
|
|
|
6
6
|
import type { EditorRect } from '@adecore/editor';
|
|
7
7
|
import { Button, Icon } from '@adecore/ui';
|
|
8
8
|
import { formatNumber } from '@adecore/ui/format';
|
|
9
|
-
import { basenameOf } from './paths.ts';
|
|
9
|
+
import { basenameOf, relativeTo } from './paths.ts';
|
|
10
10
|
import { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
11
11
|
import type { EditorLanguage } from './editor-language.ts';
|
|
12
12
|
import type { RenameView } from './popups.ts';
|
|
@@ -91,7 +91,7 @@ function Preview({ language, view }: { language: EditorLanguage; view: RenameVie
|
|
|
91
91
|
<div className="max-h-[320px] overflow-y-auto">
|
|
92
92
|
{view.files.map((file) => {
|
|
93
93
|
const path = fileUriToPath(file.uri);
|
|
94
|
-
const shown = path === null ? file.uri : path
|
|
94
|
+
const shown = path === null ? file.uri : relativeTo(path, folder);
|
|
95
95
|
return (
|
|
96
96
|
<div key={file.uri} className="border-b border-border last:border-b-0">
|
|
97
97
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs">
|
package/src/SymbolPicker.tsx
CHANGED
|
@@ -2,12 +2,11 @@ import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { AtSign, Hash } from 'lucide-react';
|
|
5
|
-
import { Icon } from '@adecore/ui';
|
|
6
|
-
import { formatShortcut } from '@adecore/ui';
|
|
7
|
-
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
-
import { basenameOf } from './paths.ts';
|
|
9
5
|
import { fileUriToPath } from '@adecore/lsp';
|
|
6
|
+
import { formatShortcut, Icon } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
10
8
|
import type { EditorLanguage } from './editor-language.ts';
|
|
9
|
+
import { basenameOf } from './paths.ts';
|
|
11
10
|
import type { SymbolsView } from './popups.ts';
|
|
12
11
|
import {
|
|
13
12
|
filterEntries,
|
|
@@ -17,6 +17,14 @@ export interface ActionEntry {
|
|
|
17
17
|
readonly group: ActionGroup;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/* Entries in the order of `order`, the preferred actions of each group first. */
|
|
21
|
+
function inGroupOrder(entries: readonly ActionEntry[], order: readonly ActionGroup[]): ActionEntry[] {
|
|
22
|
+
return order.flatMap((group) => {
|
|
23
|
+
const own = entries.filter((entry) => entry.group === group);
|
|
24
|
+
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
/* The kinds nest by dots: `refactor.extract.function` is an extraction and `source.organizeImports` a source action. */
|
|
21
29
|
export function groupOf(kind: string | undefined): ActionGroup {
|
|
22
30
|
if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
|
|
@@ -45,10 +53,7 @@ export function actionsOf(result: readonly (CodeAction | Command)[] | null, orde
|
|
|
45
53
|
.map((item) => (typeof item.command === 'string' ? ({ title: item.title, command: item as Command } satisfies CodeAction) : (item as CodeAction)))
|
|
46
54
|
.filter((action) => action.disabled === undefined)
|
|
47
55
|
.map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
|
|
48
|
-
return order
|
|
49
|
-
const own = entries.filter((entry) => entry.group === group);
|
|
50
|
-
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
51
|
-
});
|
|
56
|
+
return inGroupOrder(entries, order);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/* Entries from several requests as one list: the same order as a single answer, and an id of its own for each row. Two actions with a title and kind alike are one. */
|
|
@@ -62,10 +67,7 @@ export function mergeEntries(lists: readonly (readonly ActionEntry[])[]): Action
|
|
|
62
67
|
seen.add(key);
|
|
63
68
|
return true;
|
|
64
69
|
});
|
|
65
|
-
return ACTION_GROUPS.
|
|
66
|
-
const own = unique.filter((entry) => entry.group === group);
|
|
67
|
-
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
68
|
-
}).map((entry, index) => ({ ...entry, id: String(index) }));
|
|
70
|
+
return inGroupOrder(unique, ACTION_GROUPS).map((entry, index) => ({ ...entry, id: String(index) }));
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/* Whether the lightbulb offers it: a source action such as organizing imports is a command of its own and never a hint. */
|
package/src/code-actions.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { PickGroup, PickPreview } from './popups.ts';
|
|
|
18
18
|
import { Refresher } from './refresher.ts';
|
|
19
19
|
import { isShortcut } from './shortcut-keys.ts';
|
|
20
20
|
import { realTimers, type Timers } from './timers.ts';
|
|
21
|
+
import { messageOf } from './error-message.ts';
|
|
21
22
|
|
|
22
23
|
const METHOD = 'textDocument/codeAction';
|
|
23
24
|
const PAUSE_MS = 250;
|
|
@@ -36,6 +37,14 @@ function isRefusal(error: unknown): boolean {
|
|
|
36
37
|
return error instanceof LspError && error.code === ErrorCodes.InternalError;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface RequestOptions {
|
|
41
|
+
readonly only?: readonly string[];
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
/* The problems the actions are about; the ones touching the range when absent. */
|
|
44
|
+
readonly about?: readonly Diagnostic[];
|
|
45
|
+
readonly order?: readonly ActionGroup[];
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
interface AskOptions {
|
|
40
49
|
readonly range: EditorRange;
|
|
41
50
|
readonly only?: readonly string[];
|
|
@@ -49,16 +58,11 @@ interface AskOptions {
|
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
/*
|
|
52
|
-
* What the
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* An action is resolved before it is applied, since a server fills in its edit only when asked for it.
|
|
61
|
+
* What the servers offer at the caret or the selection: the gutter's lightbulb, the list under the caret
|
|
62
|
+
* with a preview of each change, a problem's Quick fix, organize imports and format. An action is resolved
|
|
63
|
+
* before it is applied, since a server fills in its edit only when asked for it.
|
|
56
64
|
*/
|
|
57
65
|
export class CodeActionsFeature {
|
|
58
|
-
private tell(kind: 'success' | 'error', title: string): void {
|
|
59
|
-
this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
66
|
private readonly language: EditorLanguage;
|
|
63
67
|
private readonly refresher: Refresher;
|
|
64
68
|
/* While a list is being asked for, the lightbulb waits: both are the same request and the newer one would cancel the older. */
|
|
@@ -127,7 +131,7 @@ export class CodeActionsFeature {
|
|
|
127
131
|
async organizeImports(): Promise<void> {
|
|
128
132
|
const { editor } = this.language;
|
|
129
133
|
const everything = { start: { line: 0, character: 0 }, end: editor.positionAt(Number.MAX_SAFE_INTEGER) };
|
|
130
|
-
const entries = await this.request(everything, ['source.organizeImports']
|
|
134
|
+
const entries = await this.request(everything, INVOKED, { only: ['source.organizeImports'] });
|
|
131
135
|
const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
|
|
132
136
|
if (entry === undefined) {
|
|
133
137
|
this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
|
|
@@ -154,16 +158,10 @@ export class CodeActionsFeature {
|
|
|
154
158
|
|
|
155
159
|
/* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
|
|
156
160
|
list(only: readonly string[]): Promise<ActionEntry[] | null> {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
AUTOMATIC,
|
|
162
|
-
undefined,
|
|
163
|
-
undefined,
|
|
164
|
-
only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS
|
|
165
|
-
)
|
|
166
|
-
: Promise.resolve(null);
|
|
161
|
+
if (!this.supported) {
|
|
162
|
+
return Promise.resolve(null);
|
|
163
|
+
}
|
|
164
|
+
return this.request(this.language.editor.getSelection(), AUTOMATIC, { only, order: only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS });
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
private async ask(options: AskOptions): Promise<void> {
|
|
@@ -178,7 +176,7 @@ export class CodeActionsFeature {
|
|
|
178
176
|
try {
|
|
179
177
|
entries = options.withLine
|
|
180
178
|
? await this.requestWithLine(options.range, INVOKED)
|
|
181
|
-
: await this.request(options.range, options.only,
|
|
179
|
+
: await this.request(options.range, INVOKED, { only: options.only, about: options.diagnostics, order: options.groups });
|
|
182
180
|
} finally {
|
|
183
181
|
this.asking = false;
|
|
184
182
|
}
|
|
@@ -210,24 +208,18 @@ export class CodeActionsFeature {
|
|
|
210
208
|
}
|
|
211
209
|
|
|
212
210
|
/* Asks the servers; null when they could not answer, with the reason said aloud for a request a person made. */
|
|
213
|
-
private async request(
|
|
214
|
-
range: EditorRange,
|
|
215
|
-
only: readonly string[] | undefined,
|
|
216
|
-
triggerKind: number,
|
|
217
|
-
signal?: AbortSignal,
|
|
218
|
-
about?: readonly Diagnostic[],
|
|
219
|
-
order?: readonly ActionGroup[]
|
|
220
|
-
): Promise<ActionEntry[] | null> {
|
|
211
|
+
private async request(range: EditorRange, triggerKind: number, options: RequestOptions = {}): Promise<ActionEntry[] | null> {
|
|
221
212
|
const { project, uri, diagnostics } = this.language;
|
|
213
|
+
const { only, signal, order } = options;
|
|
222
214
|
try {
|
|
215
|
+
const about =
|
|
216
|
+
options.about ??
|
|
217
|
+
diagnosticsAt(
|
|
218
|
+
diagnostics.problems.map((problem) => problem.diagnostic),
|
|
219
|
+
range
|
|
220
|
+
);
|
|
223
221
|
const context = {
|
|
224
|
-
diagnostics: [
|
|
225
|
-
...(about ??
|
|
226
|
-
diagnosticsAt(
|
|
227
|
-
diagnostics.problems.map((problem) => problem.diagnostic),
|
|
228
|
-
range
|
|
229
|
-
))
|
|
230
|
-
],
|
|
222
|
+
diagnostics: [...about],
|
|
231
223
|
...(only ? { only: [...only] } : {}),
|
|
232
224
|
triggerKind: triggerKind as 1 | 2
|
|
233
225
|
};
|
|
@@ -247,7 +239,7 @@ export class CodeActionsFeature {
|
|
|
247
239
|
* elsewhere on its line too, since the fix of an error is wanted from anywhere on the line it is on.
|
|
248
240
|
*/
|
|
249
241
|
private async requestWithLine(range: EditorRange, triggerKind: number, signal?: AbortSignal): Promise<ActionEntry[] | null> {
|
|
250
|
-
const entries = await this.request(range,
|
|
242
|
+
const entries = await this.request(range, triggerKind, { signal });
|
|
251
243
|
if (entries === null || range.start.line !== range.end.line) {
|
|
252
244
|
return entries;
|
|
253
245
|
}
|
|
@@ -256,7 +248,9 @@ export class CodeActionsFeature {
|
|
|
256
248
|
return entries;
|
|
257
249
|
}
|
|
258
250
|
const onLine = fixableOnLine(all, range.start.line).slice(0, LINE_PROBLEMS);
|
|
259
|
-
const fixes = await Promise.all(
|
|
251
|
+
const fixes = await Promise.all(
|
|
252
|
+
onLine.map((diagnostic) => this.request(diagnostic.range, triggerKind, { only: ['quickfix'], signal, about: [diagnostic] }))
|
|
253
|
+
);
|
|
260
254
|
return mergeEntries([entries, ...fixes.map((list) => list ?? [])]);
|
|
261
255
|
}
|
|
262
256
|
|
|
@@ -372,9 +366,13 @@ export class CodeActionsFeature {
|
|
|
372
366
|
}
|
|
373
367
|
}
|
|
374
368
|
|
|
369
|
+
private tell(kind: 'success' | 'error', title: string): void {
|
|
370
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
|
|
371
|
+
}
|
|
372
|
+
|
|
375
373
|
private fail(error: unknown): void {
|
|
376
374
|
if (!(error instanceof StaleResultError)) {
|
|
377
|
-
this.tell('error', this.say('failed', { message:
|
|
375
|
+
this.tell('error', this.say('failed', { message: messageOf(error) }));
|
|
378
376
|
}
|
|
379
377
|
}
|
|
380
378
|
|
package/src/code-authors.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { i18n } from 'i18next';
|
|
2
2
|
import { diffLines, splitLines } from '@adecore/merge';
|
|
3
|
-
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
4
3
|
import { formatNumber } from '@adecore/ui/format';
|
|
4
|
+
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
5
5
|
|
|
6
|
-
/*
|
|
6
|
+
/* A line no commit holds yet; every other line maps to the index of the commit that wrote it. */
|
|
7
7
|
export const UNCOMMITTED = -1;
|
|
8
8
|
|
|
9
9
|
/*
|
package/src/code-vision.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { splitLines } from '@adecore/merge';
|
|
2
|
-
import type { GitBlameResult } from './host-types.ts';
|
|
2
|
+
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
3
3
|
import type { EditorCodeVision, EditorCodeVisionEntry, EditorRect } from '@adecore/editor';
|
|
4
4
|
import type { DocumentSymbolResult } from '@adecore/lsp';
|
|
5
5
|
import type { EditorLanguage } from './editor-language.ts';
|
|
@@ -30,11 +30,10 @@ export type BlameSource =
|
|
|
30
30
|
| { readonly kind: 'ready'; readonly blame: GitBlameResult; readonly base: string; openCommit?(hash: string): void };
|
|
31
31
|
|
|
32
32
|
/*
|
|
33
|
-
* The rows above declarations: how often each is used and
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* text has settled; an old count stays on its row until the new one is in.
|
|
33
|
+
* The rows above declarations: how often each is used and who wrote it. A row holds its height from the
|
|
34
|
+
* first answer on, empty until its entries are in, so the text never moves when they arrive. Counts are
|
|
35
|
+
* asked for the declarations near the view, a few at a time, once the text has settled; an old count
|
|
36
|
+
* stays on its row until the new one is in.
|
|
38
37
|
*/
|
|
39
38
|
export class CodeVisionFeature {
|
|
40
39
|
private readonly language: EditorLanguage;
|
|
@@ -214,17 +213,12 @@ export class CodeVisionFeature {
|
|
|
214
213
|
});
|
|
215
214
|
}
|
|
216
215
|
if (mapped !== null && declaration.authors) {
|
|
217
|
-
|
|
218
|
-
if (authorship === undefined) {
|
|
219
|
-
authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
|
|
220
|
-
this.authorships.set(declaration.id, authorship);
|
|
221
|
-
}
|
|
222
|
-
const shown = authorship;
|
|
216
|
+
const authorship = this.authorshipOf(declaration, commits, mapped);
|
|
223
217
|
entries.push({
|
|
224
218
|
id: 'authors',
|
|
225
|
-
text: authorsText(
|
|
226
|
-
...(
|
|
227
|
-
activate: (anchor) => this.showAuthors(declaration,
|
|
219
|
+
text: authorsText(authorship, this.language.project.i18n),
|
|
220
|
+
...(authorship.authors.length > 0 ? { icon: authorship.authors.length > 1 ? ('users' as const) : ('user' as const) } : {}),
|
|
221
|
+
activate: (anchor) => this.showAuthors(declaration, authorship, anchor)
|
|
228
222
|
});
|
|
229
223
|
}
|
|
230
224
|
rows.push({ id: declaration.id, line: declaration.line, entries });
|
|
@@ -232,6 +226,16 @@ export class CodeVisionFeature {
|
|
|
232
226
|
return rows;
|
|
233
227
|
}
|
|
234
228
|
|
|
229
|
+
private authorshipOf(declaration: CodeVisionDeclaration, commits: readonly GitBlameCommit[], mapped: Int32Array): CodeAuthorship {
|
|
230
|
+
const known = this.authorships.get(declaration.id);
|
|
231
|
+
if (known !== undefined) {
|
|
232
|
+
return known;
|
|
233
|
+
}
|
|
234
|
+
const authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
|
|
235
|
+
this.authorships.set(declaration.id, authorship);
|
|
236
|
+
return authorship;
|
|
237
|
+
}
|
|
238
|
+
|
|
235
239
|
private mapOfBlame(source: Extract<BlameSource, { kind: 'ready' }>): Int32Array | null {
|
|
236
240
|
if (this.mapped?.blame !== source.blame) {
|
|
237
241
|
this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
|
package/src/completion-call.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompletionItem } from '@adecore/lsp';
|
|
2
|
-
import { CLASS_KIND,
|
|
2
|
+
import { CLASS_KIND, SNIPPET_FORMAT, SOURCE_LINE, isCallableKind, type Insertion } from './completion-model.ts';
|
|
3
3
|
|
|
4
4
|
/* What the platform's editors send to ask for parameter info once an item is in; the card is ours to open. */
|
|
5
5
|
export const PARAMETER_HINTS_COMMAND = 'editor.action.triggerParameterHints';
|
|
@@ -13,7 +13,6 @@ const TYPE_OPERATOR = /\b(?:typeof|keyof|instanceof|implements|satisfies|as)\s+[
|
|
|
13
13
|
const AFTER_NEW = /\bnew\s+[\w$.\\]*$/;
|
|
14
14
|
const EXISTING_CALL = /^\s*\(/;
|
|
15
15
|
const SIGNATURE_KIND_PREFIX = /^\([a-z]+(?: [a-z]+)*\)\s+(?!=>)/;
|
|
16
|
-
const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
|
|
17
16
|
|
|
18
17
|
/*
|
|
19
18
|
* What accepting an item does about the call it names: nothing, parentheses added with the caret inside or
|
|
@@ -37,10 +36,7 @@ export interface CallSite {
|
|
|
37
36
|
|
|
38
37
|
/* Whether the item is something that is called where it stands: a function, a method, a constructor, or a class after `new`. */
|
|
39
38
|
export function isCallItem(item: CompletionItem, lineBefore: string): boolean {
|
|
40
|
-
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
return item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore);
|
|
39
|
+
return isCallableKind(item.kind) || (item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore));
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
/* Whether the parameter list in a signature has anything in it; null when the text names no list. */
|
package/src/completion-model.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
-
import type { CompletionItem, CompletionList, CompletionResult,
|
|
2
|
+
import type { CompletionItem, CompletionList, CompletionResult, MarkupContent } from '@adecore/lsp';
|
|
3
3
|
import { comparePositions } from './diagnostics-model.ts';
|
|
4
|
-
import { isEmptyHover, splitSignatures, type HoverText } from './hover-content.ts';
|
|
4
|
+
import { escapeMarkdown, isEmptyHover, splitSignatures, type HoverText } from './hover-content.ts';
|
|
5
5
|
import type { CompletionDocs } from './popups.ts';
|
|
6
6
|
import { parseSnippet, tabOrder, type SnippetStop } from './snippet.ts';
|
|
7
7
|
|
|
@@ -280,6 +280,10 @@ export const CONSTRUCTOR_KIND = 4;
|
|
|
280
280
|
export const CLASS_KIND = 7;
|
|
281
281
|
export const SNIPPET_FORMAT = 2;
|
|
282
282
|
|
|
283
|
+
export function isCallableKind(kind: number | undefined): boolean {
|
|
284
|
+
return kind === METHOD_KIND || kind === FUNCTION_KIND || kind === CONSTRUCTOR_KIND;
|
|
285
|
+
}
|
|
286
|
+
|
|
283
287
|
/* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
|
|
284
288
|
function withoutCall(snippet: string): string {
|
|
285
289
|
const open = snippet.search(/(?<!\\)\(/);
|
|
@@ -309,8 +313,7 @@ export function insertionOf(item: CompletionItem, caret: EditorPosition, lineBef
|
|
|
309
313
|
return { range, text: raw, stops: [] };
|
|
310
314
|
}
|
|
311
315
|
const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
|
|
312
|
-
const
|
|
313
|
-
const parsed = parseSnippet(followed && callable ? withoutCall(raw) : raw);
|
|
316
|
+
const parsed = parseSnippet(followed && isCallableKind(item.kind) ? withoutCall(raw) : raw);
|
|
314
317
|
return { range, text: parsed.text, stops: tabOrder(parsed) };
|
|
315
318
|
}
|
|
316
319
|
|
|
@@ -352,14 +355,11 @@ export function documentationText(documentation: string | MarkupContent | undefi
|
|
|
352
355
|
if (documentation === undefined) {
|
|
353
356
|
return '';
|
|
354
357
|
}
|
|
355
|
-
return typeof documentation === 'string'
|
|
356
|
-
? documentation
|
|
357
|
-
: documentation.kind === 'plaintext'
|
|
358
|
-
? documentation.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1')
|
|
359
|
-
: documentation.value;
|
|
358
|
+
return typeof documentation === 'string' ? documentation : documentation.kind === 'plaintext' ? escapeMarkdown(documentation.value) : documentation.value;
|
|
360
359
|
}
|
|
361
360
|
|
|
362
|
-
|
|
361
|
+
/* A detail line that only says where an item comes from: a PHP `use` or an auto import. */
|
|
362
|
+
export const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
|
|
363
363
|
|
|
364
364
|
/*
|
|
365
365
|
* What is said about a suggestion beside the list, in the shape a hover has: the signature it names and the
|
|
@@ -379,8 +379,6 @@ export function completionDocsOf(item: CompletionItem, highlightLanguage: string
|
|
|
379
379
|
return source === '' && isEmptyHover(text) ? null : { source, text };
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
export type { InsertReplaceEdit, TextEdit };
|
|
383
|
-
|
|
384
382
|
const KIND_LETTERS: Record<number, string> = {
|
|
385
383
|
1: 't',
|
|
386
384
|
2: 'm',
|
package/src/completion.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses, type CallPlan } from './completion-call.ts';
|
|
21
21
|
import { RecentChoices, recentChoicesOf } from './recent-choices.ts';
|
|
22
22
|
import { shikiLanguageOf } from './language-ids.ts';
|
|
23
|
+
import { textBeforeOf } from './line-text.ts';
|
|
24
|
+
import { isEmptyRange } from './proposal-model.ts';
|
|
23
25
|
import { isShortcut } from './shortcut-keys.ts';
|
|
24
26
|
import type { CompletionRow, CompletionView } from './popups.ts';
|
|
25
27
|
import { realTimers, type Timers } from './timers.ts';
|
|
@@ -33,10 +35,6 @@ const PAGE = 8;
|
|
|
33
35
|
const LINES_BACK = 60;
|
|
34
36
|
const TOAST_ID = 'language-completion';
|
|
35
37
|
|
|
36
|
-
function isEmpty(range: EditorRange): boolean {
|
|
37
|
-
return comparePositions(range.start, range.end) === 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
38
|
/*
|
|
41
39
|
* The suggestions that open while a word is typed. A single typed identifier character or a trigger
|
|
42
40
|
* character of the server asks for the list; every character after that only filters the list that
|
|
@@ -120,7 +118,7 @@ export class CompletionFeature {
|
|
|
120
118
|
item = await this.resolveNow(entry.item);
|
|
121
119
|
}
|
|
122
120
|
const caret = editor.getCaret();
|
|
123
|
-
const lineBefore = editor
|
|
121
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
124
122
|
const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
|
|
125
123
|
// A character that commits is typed after the name, so a call is not added before it.
|
|
126
124
|
const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
|
|
@@ -310,13 +308,10 @@ export class CompletionFeature {
|
|
|
310
308
|
const triggers = this.triggerCharacters();
|
|
311
309
|
const last = Array.from(text).at(-1) ?? '';
|
|
312
310
|
const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
|
|
313
|
-
if (text.length
|
|
311
|
+
if (text.length === 1 && triggers.includes(last)) {
|
|
314
312
|
this.close();
|
|
315
313
|
this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
|
|
316
|
-
} else if (
|
|
317
|
-
isEmpty(range) &&
|
|
318
|
-
opensCompletion(this.language.editor.textInRange({ start: { line: range.start.line, character: 0 }, end: range.start }), text)
|
|
319
|
-
) {
|
|
314
|
+
} else if (isEmptyRange(range) && opensCompletion(textBeforeOf(this.language.editor, range.start), text)) {
|
|
320
315
|
this.close();
|
|
321
316
|
this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
|
|
322
317
|
} else if (this.isOpen) {
|
|
@@ -326,7 +321,7 @@ export class CompletionFeature {
|
|
|
326
321
|
} else {
|
|
327
322
|
this.close();
|
|
328
323
|
}
|
|
329
|
-
} else if (text.length === 1 && typedIdentifier &&
|
|
324
|
+
} else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
|
|
330
325
|
this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
|
|
331
326
|
}
|
|
332
327
|
}
|
|
@@ -393,7 +388,7 @@ export class CompletionFeature {
|
|
|
393
388
|
this.server = project.host.serverNames?.(uri)[0] ?? '';
|
|
394
389
|
if (!refresh) {
|
|
395
390
|
const caret = editor.getCaret();
|
|
396
|
-
const lineBefore = editor
|
|
391
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
397
392
|
const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
|
|
398
393
|
this.start = { line: caret.line, character: caret.character - prefix.length };
|
|
399
394
|
this.active = 0;
|
|
@@ -419,7 +414,7 @@ export class CompletionFeature {
|
|
|
419
414
|
this.close();
|
|
420
415
|
return;
|
|
421
416
|
}
|
|
422
|
-
const lineBefore = editor
|
|
417
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
423
418
|
// Most items of a list replace the same range, so the text under it is read once and not once per item.
|
|
424
419
|
const typed = new Map<string, string>();
|
|
425
420
|
const textBetween = (range: EditorRange): string => {
|
package/src/context-menu.ts
CHANGED
|
@@ -3,9 +3,8 @@ import type { EditorLanguage } from './editor-language.ts';
|
|
|
3
3
|
export type ClipboardAction = 'cut' | 'copy' | 'paste';
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
* The editor
|
|
7
|
-
*
|
|
8
|
-
* are asked for while it opens, so it never waits for them.
|
|
6
|
+
* The editor only says where its context menu was asked for; `EditorContextMenu` draws it from what the
|
|
7
|
+
* servers support at the caret. The refactors are asked for while it opens, so it never waits for them.
|
|
9
8
|
*/
|
|
10
9
|
export class ContextMenuFeature {
|
|
11
10
|
private readonly language: EditorLanguage;
|
|
@@ -14,7 +13,7 @@ export class ContextMenuFeature {
|
|
|
14
13
|
constructor(language: EditorLanguage) {
|
|
15
14
|
this.language = language;
|
|
16
15
|
const off = language.editor.onContextMenu((menu) => {
|
|
17
|
-
// A right click outside the selection moves the caret
|
|
16
|
+
// A right click outside the selection moves the caret first, so the menu is about what was clicked.
|
|
18
17
|
if (!menu.inSelection) {
|
|
19
18
|
language.editor.setCaret(menu.position);
|
|
20
19
|
}
|
|
@@ -33,7 +32,7 @@ export class ContextMenuFeature {
|
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/*
|
|
35
|
+
/* The browser's own commands on the editor's input, which keep the clipboard and the undo history one thing. */
|
|
37
36
|
clipboard(action: ClipboardAction): void {
|
|
38
37
|
const { editor } = this.language;
|
|
39
38
|
editor.focus();
|
package/src/definition-link.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EditorPosition } from '@adecore/editor';
|
|
|
2
2
|
import { isIdentifierCharacter } from './completion-model.ts';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { locationsOf } from './hover-content.ts';
|
|
5
|
+
import { lineTextOf } from './line-text.ts';
|
|
5
6
|
import { wordRangeAt } from './rename-model.ts';
|
|
6
7
|
|
|
7
8
|
/*
|
|
@@ -78,7 +79,7 @@ export class DefinitionLinkFeature {
|
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
const { editor, project, uri } = this.language;
|
|
81
|
-
const line = editor
|
|
82
|
+
const line = lineTextOf(editor, pointer.line);
|
|
82
83
|
const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
|
|
83
84
|
if (word === null) {
|
|
84
85
|
this.clear();
|
package/src/diagnostics-model.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EditorContentChange, EditorMarker, EditorMarkerSeverity, EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
2
|
import type { Diagnostic } from '@adecore/lsp';
|
|
3
|
+
import type { ProblemCounts } from './diagnostics.ts';
|
|
3
4
|
|
|
4
5
|
/* A diagnostic and the language server process that reported it. */
|
|
5
6
|
export interface Problem {
|
|
@@ -8,6 +9,7 @@ export interface Problem {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const SEVERITIES: Record<number, EditorMarkerSeverity> = { 1: 'error', 2: 'warning', 3: 'info', 4: 'hint' };
|
|
12
|
+
const HINT = 4;
|
|
11
13
|
const UNNECESSARY = 1;
|
|
12
14
|
const DEPRECATED = 2;
|
|
13
15
|
/* The scroll track's tip is a line or two, so a long message is cut where it stops being a tip. */
|
|
@@ -28,6 +30,12 @@ export function markerOf(diagnostic: Diagnostic): EditorMarker {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/* A diagnostic that names no severity counts as an error. */
|
|
34
|
+
export function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts {
|
|
35
|
+
const count = (severity: number): number => diagnostics.filter((diagnostic) => (diagnostic.severity ?? 1) === severity).length;
|
|
36
|
+
return { error: count(1), warning: count(2), info: count(3) };
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export function comparePositions(left: EditorPosition, right: EditorPosition): number {
|
|
32
40
|
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
33
41
|
}
|
|
@@ -74,14 +82,14 @@ export function rangeHolds(range: EditorRange, position: EditorPosition): boolea
|
|
|
74
82
|
export function problemsAt(problems: readonly Problem[], position: EditorPosition): Problem[] {
|
|
75
83
|
const rank = (problem: Problem): number => problem.diagnostic.severity ?? 1;
|
|
76
84
|
return problems
|
|
77
|
-
.filter((problem) => (problem.diagnostic.severity ?? 1) <
|
|
85
|
+
.filter((problem) => (problem.diagnostic.severity ?? 1) < HINT && rangeHolds(problem.diagnostic.range, position))
|
|
78
86
|
.sort((left, right) => rank(left) - rank(right));
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/* The start of the first problem after the position, or the last one before it when `direction` is back; it wraps round the document. */
|
|
82
90
|
export function neighborProblem(problems: readonly Problem[], from: EditorPosition, direction: 1 | -1): Problem | null {
|
|
83
91
|
const candidates = problems
|
|
84
|
-
.filter((problem) => (problem.diagnostic.severity ?? 1) <
|
|
92
|
+
.filter((problem) => (problem.diagnostic.severity ?? 1) < HINT)
|
|
85
93
|
.sort((left, right) => comparePositions(left.diagnostic.range.start, right.diagnostic.range.start));
|
|
86
94
|
if (candidates.length === 0) {
|
|
87
95
|
return null;
|