@adecore/editor-react 0.21.1 → 0.21.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/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/dist/AnchoredPopup.js
CHANGED
|
@@ -3,9 +3,8 @@ import { useLayoutEffect, useRef } from 'react';
|
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { placePopup } from './popup-placement.js';
|
|
5
5
|
/*
|
|
6
|
-
* A card next to a character of the editor,
|
|
7
|
-
*
|
|
8
|
-
* placed from the character's screen rectangle, so it is right at any zoom, and it never leaves the window.
|
|
6
|
+
* A card next to a character of the editor, portaled to the page: inside a scaled canvas node it would be
|
|
7
|
+
* drawn at that scale and clipped by the node. Placed from the character's screen rectangle, so it holds at any zoom.
|
|
9
8
|
*/
|
|
10
9
|
export function AnchoredPopup({ rect, className, children, placement, onPointerEnter, onPointerLeave }) {
|
|
11
10
|
const element = useRef(null);
|
package/dist/HoverSections.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEditorRendering } from './rendering-context.js';
|
|
3
2
|
import { useEffect, useState } from 'react';
|
|
4
|
-
import { Markdown } from './Markdown.js';
|
|
5
3
|
import { Tooltip } from '@adecore/ui';
|
|
6
|
-
import { hoverSectionsOf, markdownParts } from './hover-content.js';
|
|
7
4
|
import { BaselineStatus } from './BaselineStatus.js';
|
|
5
|
+
import { hoverSectionsOf, markdownParts } from './hover-content.js';
|
|
6
|
+
import { Markdown } from './Markdown.js';
|
|
7
|
+
import { useEditorRendering } from './rendering-context.js';
|
|
8
8
|
import { declaredNameOf, linkTypeNames } from './symbol-links.js';
|
|
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. */
|
|
10
10
|
export function Signature({ code, language, onName }) {
|
|
@@ -53,20 +53,14 @@ export function SymbolSections({ text, onName }) {
|
|
|
53
53
|
function phpWithoutOpenTag(html) {
|
|
54
54
|
return html.replace(/<span[^>]*><\?<\/span><span[^>]*>php<\/span>(<span[^>]*>) /, '$1');
|
|
55
55
|
}
|
|
56
|
-
/*
|
|
57
|
-
* Where a suggestion comes from, such as `use Raxos\Database\Orm\Attribute\PrimaryKey`, on one line that gives
|
|
58
|
-
* 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.
|
|
59
|
-
*/
|
|
56
|
+
/* Where a suggestion comes from, such as a PHP `use` line, cut from its start so the end of the name stays readable. */
|
|
60
57
|
export function SourceLine({ text, language }) {
|
|
61
58
|
const { theme, highlight } = useEditorRendering();
|
|
62
59
|
const [html, setHtml] = useState(null);
|
|
63
60
|
const highlighted = language === 'php' && text.startsWith('use ');
|
|
64
61
|
const key = `${theme}\0${text}`;
|
|
65
62
|
useEffect(() => {
|
|
66
|
-
if (!highlighted) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (highlight === undefined) {
|
|
63
|
+
if (!highlighted || highlight === undefined) {
|
|
70
64
|
return;
|
|
71
65
|
}
|
|
72
66
|
let alive = true;
|
package/dist/PeekPanel.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEditorRendering } from './rendering-context.js';
|
|
3
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
3
|
import { createPortal } from 'react-dom';
|
|
5
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -7,11 +6,12 @@ import { ChevronDown, ChevronRight, X } from 'lucide-react';
|
|
|
7
6
|
import { fileUriToPath } from '@adecore/lsp';
|
|
8
7
|
import { FileIcon, IconButton, Tooltip } from '@adecore/ui';
|
|
9
8
|
import { formatNumber } from '@adecore/ui/format';
|
|
10
|
-
import { basenameOf } from './paths.js';
|
|
9
|
+
import { basenameOf, relativeTo } from './paths.js';
|
|
11
10
|
import { shikiLanguageOfPath } from './language-ids.js';
|
|
12
11
|
import { PathText } from './PathText.js';
|
|
13
12
|
import { PEEK_HEIGHT } from './peek.js';
|
|
14
13
|
import { distinguishingFolders, visualColumnOf } from './peek-model.js';
|
|
14
|
+
import { useEditorRendering } from './rendering-context.js';
|
|
15
15
|
const TAB_SIZE = 4;
|
|
16
16
|
const CODE = 'font-mono text-(length:--code-font-size) [tab-size:4] [&_.line]:block [&_.line]:h-(--code-line-height) [&_.line]:leading-(--code-line-height) [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-0 [&_pre]:whitespace-pre [&_code]:font-mono';
|
|
17
17
|
/* The code around the active reference, drawn like the editor's own rows: its gutter, the line the reference is on and the name marked. */
|
|
@@ -23,10 +23,7 @@ function Preview({ view }) {
|
|
|
23
23
|
const path = preview === null ? null : fileUriToPath(preview.uri);
|
|
24
24
|
const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
|
|
25
25
|
useEffect(() => {
|
|
26
|
-
if (preview === null) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (highlight === undefined) {
|
|
26
|
+
if (preview === null || highlight === undefined) {
|
|
30
27
|
return;
|
|
31
28
|
}
|
|
32
29
|
let alive = true;
|
|
@@ -57,9 +54,6 @@ function columnsOf(line, name) {
|
|
|
57
54
|
const start = visualColumnOf(line, name.start, TAB_SIZE);
|
|
58
55
|
return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
|
|
59
56
|
}
|
|
60
|
-
function relativeTo(path, folder) {
|
|
61
|
-
return path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
|
|
62
|
-
}
|
|
63
57
|
function folderOf(path) {
|
|
64
58
|
return path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
65
59
|
}
|
|
@@ -90,10 +84,9 @@ function Places({ language, view }) {
|
|
|
90
84
|
}) }));
|
|
91
85
|
}
|
|
92
86
|
/*
|
|
93
|
-
* The references of a name
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* the editor's own ground and no edge of its own, so it reads as part of the file.
|
|
87
|
+
* The references of a name in the row the editor leaves under the line: the code around the active one on
|
|
88
|
+
* the left and every place on the right. It portals into that row, which the editor makes again when it
|
|
89
|
+
* scrolls back into view, and has no edge of its own, so it reads as part of the file.
|
|
97
90
|
*/
|
|
98
91
|
export function PeekPanel({ language, view }) {
|
|
99
92
|
const { t } = useTranslation('editor');
|
package/dist/RenameCard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Sparkles } from 'lucide-react';
|
|
|
6
6
|
import { fileUriToPath } from '@adecore/lsp';
|
|
7
7
|
import { Button, Icon } from '@adecore/ui';
|
|
8
8
|
import { formatNumber } from '@adecore/ui/format';
|
|
9
|
-
import { basenameOf } from './paths.js';
|
|
9
|
+
import { basenameOf, relativeTo } from './paths.js';
|
|
10
10
|
import { AnchoredPopup } from './AnchoredPopup.js';
|
|
11
11
|
const CARD_ATTRIBUTE = 'data-rename-card';
|
|
12
12
|
/* Whether the focus moved to something that belongs to the rename, which is not leaving it. */
|
|
@@ -37,7 +37,7 @@ function Preview({ language, view }) {
|
|
|
37
37
|
const folder = language.project.folder;
|
|
38
38
|
return (_jsxs("div", { className: "flex w-[520px] max-w-full flex-col", onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "max-h-[320px] overflow-y-auto", children: view.files.map((file) => {
|
|
39
39
|
const path = fileUriToPath(file.uri);
|
|
40
|
-
const shown = path === null ? file.uri : path
|
|
40
|
+
const shown = path === null ? file.uri : relativeTo(path, folder);
|
|
41
41
|
return (_jsxs("div", { className: "border-b border-border last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 text-xs", children: [_jsx("span", { className: "font-medium", children: path === null ? shown : basenameOf(path) }), _jsx("span", { className: "min-w-0 truncate text-text-faint", children: shown })] }), _jsx("div", { className: "flex flex-col gap-px px-1 pb-1.5 font-mono text-code", children: file.rows.map((row) => (_jsxs("div", { className: "flex flex-col gap-px", children: [_jsxs("div", { className: "flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0 text-right text-text-faint", children: row.line }), _jsx("span", { className: "min-w-0 truncate", children: row.before })] }), _jsxs("div", { className: "flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.after })] })] }, row.line))) })] }, file.uri));
|
|
42
42
|
}) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => language.rename.cancel(), children: t('language.rename.cancel') }), _jsx(Button, { size: "xs", variant: "primary", onClick: () => void language.rename.confirm(), children: t('language.rename.apply') })] })] }));
|
|
43
43
|
}
|
package/dist/SymbolPicker.js
CHANGED
|
@@ -3,11 +3,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { AtSign, Hash } from 'lucide-react';
|
|
6
|
-
import {
|
|
7
|
-
import { formatShortcut } from '@adecore/ui';
|
|
6
|
+
import { fileUriToPath } from '@adecore/lsp';
|
|
7
|
+
import { formatShortcut, Icon } from '@adecore/ui';
|
|
8
8
|
import { formatNumber } from '@adecore/ui/format';
|
|
9
9
|
import { basenameOf } from './paths.js';
|
|
10
|
-
import { fileUriToPath } from '@adecore/lsp';
|
|
11
10
|
import { filterEntries, groupEntries, letterOfKind, modeOf, toneOfKind } from './symbol-picker-model.js';
|
|
12
11
|
const TONES = {
|
|
13
12
|
callable: 'bg-accent-soft text-accent',
|
|
@@ -5,6 +5,13 @@ import { entriesOf, renamesOf } from './workspace-edit.js';
|
|
|
5
5
|
export const ACTION_GROUPS = ['quickfix', 'rewrite', 'extract', 'inline', 'move', 'refactor', 'source', 'other'];
|
|
6
6
|
/* The order of Refactor This: what changes the structure of the code first, the rewrites after it. */
|
|
7
7
|
export const REFACTOR_GROUPS = ['extract', 'inline', 'move', 'rewrite', 'refactor'];
|
|
8
|
+
/* Entries in the order of `order`, the preferred actions of each group first. */
|
|
9
|
+
function inGroupOrder(entries, order) {
|
|
10
|
+
return order.flatMap((group) => {
|
|
11
|
+
const own = entries.filter((entry) => entry.group === group);
|
|
12
|
+
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
8
15
|
/* The kinds nest by dots: `refactor.extract.function` is an extraction and `source.organizeImports` a source action. */
|
|
9
16
|
export function groupOf(kind) {
|
|
10
17
|
if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
|
|
@@ -32,10 +39,7 @@ export function actionsOf(result, order = ACTION_GROUPS) {
|
|
|
32
39
|
.map((item) => (typeof item.command === 'string' ? { title: item.title, command: item } : item))
|
|
33
40
|
.filter((action) => action.disabled === undefined)
|
|
34
41
|
.map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
|
|
35
|
-
return order
|
|
36
|
-
const own = entries.filter((entry) => entry.group === group);
|
|
37
|
-
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
38
|
-
});
|
|
42
|
+
return inGroupOrder(entries, order);
|
|
39
43
|
}
|
|
40
44
|
/* 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. */
|
|
41
45
|
export function mergeEntries(lists) {
|
|
@@ -48,10 +52,7 @@ export function mergeEntries(lists) {
|
|
|
48
52
|
seen.add(key);
|
|
49
53
|
return true;
|
|
50
54
|
});
|
|
51
|
-
return ACTION_GROUPS.
|
|
52
|
-
const own = unique.filter((entry) => entry.group === group);
|
|
53
|
-
return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
|
|
54
|
-
}).map((entry, index) => ({ ...entry, id: String(index) }));
|
|
55
|
+
return inGroupOrder(unique, ACTION_GROUPS).map((entry, index) => ({ ...entry, id: String(index) }));
|
|
55
56
|
}
|
|
56
57
|
/* Whether the lightbulb offers it: a source action such as organizing imports is a command of its own and never a hint. */
|
|
57
58
|
export function isHint(entry) {
|
package/dist/code-actions.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Problem } from './diagnostics-model.ts';
|
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { type Timers } from './timers.ts';
|
|
5
5
|
export declare class CodeActionsFeature {
|
|
6
|
-
private tell;
|
|
7
6
|
private readonly language;
|
|
8
7
|
private readonly refresher;
|
|
9
8
|
private asking;
|
|
@@ -25,6 +24,7 @@ export declare class CodeActionsFeature {
|
|
|
25
24
|
private refusalOf;
|
|
26
25
|
apply(entry: ActionEntry): Promise<void>;
|
|
27
26
|
private startRename;
|
|
27
|
+
private tell;
|
|
28
28
|
private fail;
|
|
29
29
|
private say;
|
|
30
30
|
}
|
package/dist/code-actions.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ACTION_GROUPS, REFACTOR_GROUPS, actionsOf, diagnosticsAt, fixableOnLine
|
|
|
3
3
|
import { Refresher } from './refresher.js';
|
|
4
4
|
import { isShortcut } from './shortcut-keys.js';
|
|
5
5
|
import { realTimers } from './timers.js';
|
|
6
|
+
import { messageOf } from './error-message.js';
|
|
6
7
|
const METHOD = 'textDocument/codeAction';
|
|
7
8
|
const PAUSE_MS = 250;
|
|
8
9
|
const INVOKED = 1;
|
|
@@ -19,15 +20,11 @@ function isRefusal(error) {
|
|
|
19
20
|
return error instanceof LspError && error.code === ErrorCodes.InternalError;
|
|
20
21
|
}
|
|
21
22
|
/*
|
|
22
|
-
* What the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* An action is resolved before it is applied, since a server fills in its edit only when asked for it.
|
|
23
|
+
* What the servers offer at the caret or the selection: the gutter's lightbulb, the list under the caret
|
|
24
|
+
* with a preview of each change, a problem's Quick fix, organize imports and format. An action is resolved
|
|
25
|
+
* before it is applied, since a server fills in its edit only when asked for it.
|
|
26
26
|
*/
|
|
27
27
|
export class CodeActionsFeature {
|
|
28
|
-
tell(kind, title) {
|
|
29
|
-
this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
|
|
30
|
-
}
|
|
31
28
|
language;
|
|
32
29
|
refresher;
|
|
33
30
|
/* While a list is being asked for, the lightbulb waits: both are the same request and the newer one would cancel the older. */
|
|
@@ -90,7 +87,7 @@ export class CodeActionsFeature {
|
|
|
90
87
|
async organizeImports() {
|
|
91
88
|
const { editor } = this.language;
|
|
92
89
|
const everything = { start: { line: 0, character: 0 }, end: editor.positionAt(Number.MAX_SAFE_INTEGER) };
|
|
93
|
-
const entries = await this.request(everything, ['source.organizeImports']
|
|
90
|
+
const entries = await this.request(everything, INVOKED, { only: ['source.organizeImports'] });
|
|
94
91
|
const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
|
|
95
92
|
if (entry === undefined) {
|
|
96
93
|
this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
|
|
@@ -116,9 +113,10 @@ export class CodeActionsFeature {
|
|
|
116
113
|
}
|
|
117
114
|
/* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
|
|
118
115
|
list(only) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
if (!this.supported) {
|
|
117
|
+
return Promise.resolve(null);
|
|
118
|
+
}
|
|
119
|
+
return this.request(this.language.editor.getSelection(), AUTOMATIC, { only, order: only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS });
|
|
122
120
|
}
|
|
123
121
|
async ask(options) {
|
|
124
122
|
if (!this.supported) {
|
|
@@ -132,7 +130,7 @@ export class CodeActionsFeature {
|
|
|
132
130
|
try {
|
|
133
131
|
entries = options.withLine
|
|
134
132
|
? await this.requestWithLine(options.range, INVOKED)
|
|
135
|
-
: await this.request(options.range, options.only,
|
|
133
|
+
: await this.request(options.range, INVOKED, { only: options.only, about: options.diagnostics, order: options.groups });
|
|
136
134
|
}
|
|
137
135
|
finally {
|
|
138
136
|
this.asking = false;
|
|
@@ -164,14 +162,14 @@ export class CodeActionsFeature {
|
|
|
164
162
|
});
|
|
165
163
|
}
|
|
166
164
|
/* Asks the servers; null when they could not answer, with the reason said aloud for a request a person made. */
|
|
167
|
-
async request(range,
|
|
165
|
+
async request(range, triggerKind, options = {}) {
|
|
168
166
|
const { project, uri, diagnostics } = this.language;
|
|
167
|
+
const { only, signal, order } = options;
|
|
169
168
|
try {
|
|
169
|
+
const about = options.about ??
|
|
170
|
+
diagnosticsAt(diagnostics.problems.map((problem) => problem.diagnostic), range);
|
|
170
171
|
const context = {
|
|
171
|
-
diagnostics: [
|
|
172
|
-
...(about ??
|
|
173
|
-
diagnosticsAt(diagnostics.problems.map((problem) => problem.diagnostic), range))
|
|
174
|
-
],
|
|
172
|
+
diagnostics: [...about],
|
|
175
173
|
...(only ? { only: [...only] } : {}),
|
|
176
174
|
triggerKind: triggerKind
|
|
177
175
|
};
|
|
@@ -191,7 +189,7 @@ export class CodeActionsFeature {
|
|
|
191
189
|
* elsewhere on its line too, since the fix of an error is wanted from anywhere on the line it is on.
|
|
192
190
|
*/
|
|
193
191
|
async requestWithLine(range, triggerKind, signal) {
|
|
194
|
-
const entries = await this.request(range,
|
|
192
|
+
const entries = await this.request(range, triggerKind, { signal });
|
|
195
193
|
if (entries === null || range.start.line !== range.end.line) {
|
|
196
194
|
return entries;
|
|
197
195
|
}
|
|
@@ -200,7 +198,7 @@ export class CodeActionsFeature {
|
|
|
200
198
|
return entries;
|
|
201
199
|
}
|
|
202
200
|
const onLine = fixableOnLine(all, range.start.line).slice(0, LINE_PROBLEMS);
|
|
203
|
-
const fixes = await Promise.all(onLine.map((diagnostic) => this.request(diagnostic.range, ['quickfix'],
|
|
201
|
+
const fixes = await Promise.all(onLine.map((diagnostic) => this.request(diagnostic.range, triggerKind, { only: ['quickfix'], signal, about: [diagnostic] })));
|
|
204
202
|
return mergeEntries([entries, ...fixes.map((list) => list ?? [])]);
|
|
205
203
|
}
|
|
206
204
|
async hint(signal) {
|
|
@@ -315,9 +313,12 @@ export class CodeActionsFeature {
|
|
|
315
313
|
await this.language.rename.start();
|
|
316
314
|
}
|
|
317
315
|
}
|
|
316
|
+
tell(kind, title) {
|
|
317
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
|
|
318
|
+
}
|
|
318
319
|
fail(error) {
|
|
319
320
|
if (!(error instanceof StaleResultError)) {
|
|
320
|
-
this.tell('error', this.say('failed', { message:
|
|
321
|
+
this.tell('error', this.say('failed', { message: messageOf(error) }));
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
say(key, options) {
|
package/dist/code-authors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { diffLines, splitLines } from '@adecore/merge';
|
|
2
2
|
import { formatNumber } from '@adecore/ui/format';
|
|
3
|
-
/*
|
|
3
|
+
/* A line no commit holds yet; every other line maps to the index of the commit that wrote it. */
|
|
4
4
|
export const UNCOMMITTED = -1;
|
|
5
5
|
/*
|
|
6
6
|
* The commit of every line of the text in the editor. Blame was run for the text on disk, so a line
|
package/dist/code-vision.d.ts
CHANGED
package/dist/code-vision.js
CHANGED
|
@@ -11,11 +11,10 @@ const SCROLL_PAUSE_MS = 150;
|
|
|
11
11
|
/* Past this many lines a file gets no rows at all, which is also what keeps blame and the walk of symbols cheap. */
|
|
12
12
|
export const MAX_LINES = 20_000;
|
|
13
13
|
/*
|
|
14
|
-
* The rows above declarations: how often each is used and
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* text has settled; an old count stays on its row until the new one is in.
|
|
14
|
+
* The rows above declarations: how often each is used and who wrote it. A row holds its height from the
|
|
15
|
+
* first answer on, empty until its entries are in, so the text never moves when they arrive. Counts are
|
|
16
|
+
* asked for the declarations near the view, a few at a time, once the text has settled; an old count
|
|
17
|
+
* stays on its row until the new one is in.
|
|
19
18
|
*/
|
|
20
19
|
export class CodeVisionFeature {
|
|
21
20
|
language;
|
|
@@ -185,23 +184,27 @@ export class CodeVisionFeature {
|
|
|
185
184
|
});
|
|
186
185
|
}
|
|
187
186
|
if (mapped !== null && declaration.authors) {
|
|
188
|
-
|
|
189
|
-
if (authorship === undefined) {
|
|
190
|
-
authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
|
|
191
|
-
this.authorships.set(declaration.id, authorship);
|
|
192
|
-
}
|
|
193
|
-
const shown = authorship;
|
|
187
|
+
const authorship = this.authorshipOf(declaration, commits, mapped);
|
|
194
188
|
entries.push({
|
|
195
189
|
id: 'authors',
|
|
196
|
-
text: authorsText(
|
|
197
|
-
...(
|
|
198
|
-
activate: (anchor) => this.showAuthors(declaration,
|
|
190
|
+
text: authorsText(authorship, this.language.project.i18n),
|
|
191
|
+
...(authorship.authors.length > 0 ? { icon: authorship.authors.length > 1 ? 'users' : 'user' } : {}),
|
|
192
|
+
activate: (anchor) => this.showAuthors(declaration, authorship, anchor)
|
|
199
193
|
});
|
|
200
194
|
}
|
|
201
195
|
rows.push({ id: declaration.id, line: declaration.line, entries });
|
|
202
196
|
}
|
|
203
197
|
return rows;
|
|
204
198
|
}
|
|
199
|
+
authorshipOf(declaration, commits, mapped) {
|
|
200
|
+
const known = this.authorships.get(declaration.id);
|
|
201
|
+
if (known !== undefined) {
|
|
202
|
+
return known;
|
|
203
|
+
}
|
|
204
|
+
const authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
|
|
205
|
+
this.authorships.set(declaration.id, authorship);
|
|
206
|
+
return authorship;
|
|
207
|
+
}
|
|
205
208
|
mapOfBlame(source) {
|
|
206
209
|
if (this.mapped?.blame !== source.blame) {
|
|
207
210
|
this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
|
package/dist/completion-call.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLASS_KIND,
|
|
1
|
+
import { CLASS_KIND, SNIPPET_FORMAT, SOURCE_LINE, isCallableKind } from './completion-model.js';
|
|
2
2
|
/* What the platform's editors send to ask for parameter info once an item is in; the card is ours to open. */
|
|
3
3
|
export const PARAMETER_HINTS_COMMAND = 'editor.action.triggerParameterHints';
|
|
4
4
|
const CALL_LANGUAGES = new Set(['typescript', 'typescriptreact', 'javascript', 'javascriptreact', 'vue', 'php', 'python']);
|
|
@@ -9,13 +9,9 @@ const TYPE_OPERATOR = /\b(?:typeof|keyof|instanceof|implements|satisfies|as)\s+[
|
|
|
9
9
|
const AFTER_NEW = /\bnew\s+[\w$.\\]*$/;
|
|
10
10
|
const EXISTING_CALL = /^\s*\(/;
|
|
11
11
|
const SIGNATURE_KIND_PREFIX = /^\([a-z]+(?: [a-z]+)*\)\s+(?!=>)/;
|
|
12
|
-
const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
|
|
13
12
|
/* Whether the item is something that is called where it stands: a function, a method, a constructor, or a class after `new`. */
|
|
14
13
|
export function isCallItem(item, lineBefore) {
|
|
15
|
-
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
return item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore);
|
|
14
|
+
return isCallableKind(item.kind) || (item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore));
|
|
19
15
|
}
|
|
20
16
|
/* Whether the parameter list in a signature has anything in it; null when the text names no list. */
|
|
21
17
|
function listedParameters(signature) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
-
import type { CompletionItem, CompletionResult,
|
|
2
|
+
import type { CompletionItem, CompletionResult, MarkupContent } from '@adecore/lsp';
|
|
3
3
|
import type { CompletionDocs } from './popups.ts';
|
|
4
4
|
import { type SnippetStop } from './snippet.ts';
|
|
5
5
|
export declare function isIdentifierCharacter(character: string): boolean;
|
|
@@ -32,6 +32,7 @@ export declare const FUNCTION_KIND = 3;
|
|
|
32
32
|
export declare const CONSTRUCTOR_KIND = 4;
|
|
33
33
|
export declare const CLASS_KIND = 7;
|
|
34
34
|
export declare const SNIPPET_FORMAT = 2;
|
|
35
|
+
export declare function isCallableKind(kind: number | undefined): boolean;
|
|
35
36
|
export interface Insertion {
|
|
36
37
|
readonly range: EditorRange;
|
|
37
38
|
readonly text: string;
|
|
@@ -40,8 +41,8 @@ export interface Insertion {
|
|
|
40
41
|
export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string): Insertion;
|
|
41
42
|
export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion): EditorContentChange[];
|
|
42
43
|
export declare function documentationText(documentation: string | MarkupContent | undefined): string;
|
|
44
|
+
export declare const SOURCE_LINE: RegExp;
|
|
43
45
|
export declare function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null;
|
|
44
|
-
export type { InsertReplaceEdit, TextEdit };
|
|
45
46
|
export type KindTone = 'callable' | 'value' | 'type' | 'other';
|
|
46
47
|
export declare function kindLetterOf(kind: number | undefined): string;
|
|
47
48
|
export declare function kindToneOf(kind: number | undefined): KindTone;
|
package/dist/completion-model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { comparePositions } from './diagnostics-model.js';
|
|
2
|
-
import { isEmptyHover, splitSignatures } from './hover-content.js';
|
|
2
|
+
import { escapeMarkdown, isEmptyHover, splitSignatures } from './hover-content.js';
|
|
3
3
|
import { parseSnippet, tabOrder } from './snippet.js';
|
|
4
4
|
const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
|
|
5
5
|
export function isIdentifierCharacter(character) {
|
|
@@ -224,6 +224,9 @@ export const FUNCTION_KIND = 3;
|
|
|
224
224
|
export const CONSTRUCTOR_KIND = 4;
|
|
225
225
|
export const CLASS_KIND = 7;
|
|
226
226
|
export const SNIPPET_FORMAT = 2;
|
|
227
|
+
export function isCallableKind(kind) {
|
|
228
|
+
return kind === METHOD_KIND || kind === FUNCTION_KIND || kind === CONSTRUCTOR_KIND;
|
|
229
|
+
}
|
|
227
230
|
/* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
|
|
228
231
|
function withoutCall(snippet) {
|
|
229
232
|
const open = snippet.search(/(?<!\\)\(/);
|
|
@@ -244,8 +247,7 @@ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
|
|
|
244
247
|
return { range, text: raw, stops: [] };
|
|
245
248
|
}
|
|
246
249
|
const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
|
|
247
|
-
const
|
|
248
|
-
const parsed = parseSnippet(followed && callable ? withoutCall(raw) : raw);
|
|
250
|
+
const parsed = parseSnippet(followed && isCallableKind(item.kind) ? withoutCall(raw) : raw);
|
|
249
251
|
return { range, text: parsed.text, stops: tabOrder(parsed) };
|
|
250
252
|
}
|
|
251
253
|
/*
|
|
@@ -279,13 +281,10 @@ export function documentationText(documentation) {
|
|
|
279
281
|
if (documentation === undefined) {
|
|
280
282
|
return '';
|
|
281
283
|
}
|
|
282
|
-
return typeof documentation === 'string'
|
|
283
|
-
? documentation
|
|
284
|
-
: documentation.kind === 'plaintext'
|
|
285
|
-
? documentation.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1')
|
|
286
|
-
: documentation.value;
|
|
284
|
+
return typeof documentation === 'string' ? documentation : documentation.kind === 'plaintext' ? escapeMarkdown(documentation.value) : documentation.value;
|
|
287
285
|
}
|
|
288
|
-
|
|
286
|
+
/* A detail line that only says where an item comes from: a PHP `use` or an auto import. */
|
|
287
|
+
export const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
|
|
289
288
|
/*
|
|
290
289
|
* What is said about a suggestion beside the list, in the shape a hover has: the signature it names and the
|
|
291
290
|
* documentation split into signature, prose and tags. A first line that only says where the item comes from is
|
package/dist/completion.js
CHANGED
|
@@ -4,6 +4,8 @@ import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter,
|
|
|
4
4
|
import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses } from './completion-call.js';
|
|
5
5
|
import { RecentChoices, recentChoicesOf } from './recent-choices.js';
|
|
6
6
|
import { shikiLanguageOf } from './language-ids.js';
|
|
7
|
+
import { textBeforeOf } from './line-text.js';
|
|
8
|
+
import { isEmptyRange } from './proposal-model.js';
|
|
7
9
|
import { isShortcut } from './shortcut-keys.js';
|
|
8
10
|
import { realTimers } from './timers.js';
|
|
9
11
|
const METHOD = 'textDocument/completion';
|
|
@@ -14,9 +16,6 @@ const RESOLVE_DELAY_MS = 120;
|
|
|
14
16
|
const PAGE = 8;
|
|
15
17
|
const LINES_BACK = 60;
|
|
16
18
|
const TOAST_ID = 'language-completion';
|
|
17
|
-
function isEmpty(range) {
|
|
18
|
-
return comparePositions(range.start, range.end) === 0;
|
|
19
|
-
}
|
|
20
19
|
/*
|
|
21
20
|
* The suggestions that open while a word is typed. A single typed identifier character or a trigger
|
|
22
21
|
* character of the server asks for the list; every character after that only filters the list that
|
|
@@ -95,7 +94,7 @@ export class CompletionFeature {
|
|
|
95
94
|
item = await this.resolveNow(entry.item);
|
|
96
95
|
}
|
|
97
96
|
const caret = editor.getCaret();
|
|
98
|
-
const lineBefore = editor
|
|
97
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
99
98
|
const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
|
|
100
99
|
// A character that commits is typed after the name, so a call is not added before it.
|
|
101
100
|
const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
|
|
@@ -277,12 +276,11 @@ export class CompletionFeature {
|
|
|
277
276
|
const triggers = this.triggerCharacters();
|
|
278
277
|
const last = Array.from(text).at(-1) ?? '';
|
|
279
278
|
const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
|
|
280
|
-
if (text.length
|
|
279
|
+
if (text.length === 1 && triggers.includes(last)) {
|
|
281
280
|
this.close();
|
|
282
281
|
this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
|
|
283
282
|
}
|
|
284
|
-
else if (
|
|
285
|
-
opensCompletion(this.language.editor.textInRange({ start: { line: range.start.line, character: 0 }, end: range.start }), text)) {
|
|
283
|
+
else if (isEmptyRange(range) && opensCompletion(textBeforeOf(this.language.editor, range.start), text)) {
|
|
286
284
|
this.close();
|
|
287
285
|
this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
|
|
288
286
|
}
|
|
@@ -295,7 +293,7 @@ export class CompletionFeature {
|
|
|
295
293
|
this.close();
|
|
296
294
|
}
|
|
297
295
|
}
|
|
298
|
-
else if (text.length === 1 && typedIdentifier &&
|
|
296
|
+
else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
|
|
299
297
|
this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
|
|
300
298
|
}
|
|
301
299
|
}
|
|
@@ -359,7 +357,7 @@ export class CompletionFeature {
|
|
|
359
357
|
this.server = project.host.serverNames?.(uri)[0] ?? '';
|
|
360
358
|
if (!refresh) {
|
|
361
359
|
const caret = editor.getCaret();
|
|
362
|
-
const lineBefore = editor
|
|
360
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
363
361
|
const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
|
|
364
362
|
this.start = { line: caret.line, character: caret.character - prefix.length };
|
|
365
363
|
this.active = 0;
|
|
@@ -386,7 +384,7 @@ export class CompletionFeature {
|
|
|
386
384
|
this.close();
|
|
387
385
|
return;
|
|
388
386
|
}
|
|
389
|
-
const lineBefore = editor
|
|
387
|
+
const lineBefore = textBeforeOf(editor, caret);
|
|
390
388
|
// Most items of a list replace the same range, so the text under it is read once and not once per item.
|
|
391
389
|
const typed = new Map();
|
|
392
390
|
const textBetween = (range) => {
|
package/dist/context-menu.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The editor
|
|
3
|
-
*
|
|
4
|
-
* are asked for while it opens, so it never waits for them.
|
|
2
|
+
* The editor only says where its context menu was asked for; `EditorContextMenu` draws it from what the
|
|
3
|
+
* servers support at the caret. The refactors are asked for while it opens, so it never waits for them.
|
|
5
4
|
*/
|
|
6
5
|
export class ContextMenuFeature {
|
|
7
6
|
language;
|
|
@@ -9,7 +8,7 @@ export class ContextMenuFeature {
|
|
|
9
8
|
constructor(language) {
|
|
10
9
|
this.language = language;
|
|
11
10
|
const off = language.editor.onContextMenu((menu) => {
|
|
12
|
-
// A right click outside the selection moves the caret
|
|
11
|
+
// A right click outside the selection moves the caret first, so the menu is about what was clicked.
|
|
13
12
|
if (!menu.inSelection) {
|
|
14
13
|
language.editor.setCaret(menu.position);
|
|
15
14
|
}
|
|
@@ -26,7 +25,7 @@ export class ContextMenuFeature {
|
|
|
26
25
|
this.language.popups.setState({ menu: null });
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
|
-
/*
|
|
28
|
+
/* The browser's own commands on the editor's input, which keep the clipboard and the undo history one thing. */
|
|
30
29
|
clipboard(action) {
|
|
31
30
|
const { editor } = this.language;
|
|
32
31
|
editor.focus();
|
package/dist/definition-link.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isIdentifierCharacter } from './completion-model.js';
|
|
2
2
|
import { locationsOf } from './hover-content.js';
|
|
3
|
+
import { lineTextOf } from './line-text.js';
|
|
3
4
|
import { wordRangeAt } from './rename-model.js';
|
|
4
5
|
/*
|
|
5
6
|
* The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
|
|
@@ -71,7 +72,7 @@ export class DefinitionLinkFeature {
|
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
const { editor, project, uri } = this.language;
|
|
74
|
-
const line = editor
|
|
75
|
+
const line = lineTextOf(editor, pointer.line);
|
|
75
76
|
const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
|
|
76
77
|
if (word === null) {
|
|
77
78
|
this.clear();
|
|
@@ -1,11 +1,13 @@
|
|
|
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
|
export interface Problem {
|
|
4
5
|
readonly diagnostic: Diagnostic;
|
|
5
6
|
readonly server: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function severityOf(diagnostic: Diagnostic): EditorMarkerSeverity;
|
|
8
9
|
export declare function markerOf(diagnostic: Diagnostic): EditorMarker;
|
|
10
|
+
export declare function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts;
|
|
9
11
|
export declare function comparePositions(left: EditorPosition, right: EditorPosition): number;
|
|
10
12
|
export declare function shiftPosition(position: EditorPosition, change: EditorContentChange): EditorPosition;
|
|
11
13
|
export declare function shiftRange(range: EditorRange, changes: readonly EditorContentChange[]): EditorRange;
|