@adecore/editor-react 0.0.1 → 0.17.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +60 -0
- package/dist/AnchoredPopup.d.ts +12 -0
- package/dist/AnchoredPopup.js +27 -0
- package/dist/AttributionCard.d.ts +14 -0
- package/dist/AttributionCard.js +5 -0
- package/dist/BaselineStatus.d.ts +4 -0
- package/dist/BaselineStatus.js +13 -0
- package/dist/ChangeReview.d.ts +12 -0
- package/dist/ChangeReview.js +27 -0
- package/dist/CodeAuthorsCard.d.ts +6 -0
- package/dist/CodeAuthorsCard.js +22 -0
- package/dist/CompletionPopup.d.ts +8 -0
- package/dist/CompletionPopup.js +103 -0
- package/dist/EditorContextMenu.d.ts +8 -0
- package/dist/EditorContextMenu.js +23 -0
- package/dist/EditorView.d.ts +16 -0
- package/dist/EditorView.js +27 -0
- package/dist/FindReplace.d.ts +6 -0
- package/dist/FindReplace.js +44 -0
- package/dist/HoverCard.d.ts +11 -0
- package/dist/HoverCard.js +38 -0
- package/dist/HoverSections.d.ts +21 -0
- package/dist/HoverSections.js +82 -0
- package/dist/LanguagePopups.d.ts +6 -0
- package/dist/LanguagePopups.js +38 -0
- package/dist/Markdown.d.ts +3 -0
- package/dist/Markdown.js +7 -0
- package/dist/PathText.d.ts +4 -0
- package/dist/PathText.js +12 -0
- package/dist/PeekPanel.d.ts +6 -0
- package/dist/PeekPanel.js +111 -0
- package/dist/PickPopup.d.ts +8 -0
- package/dist/PickPopup.js +33 -0
- package/dist/RenameCard.d.ts +9 -0
- package/dist/RenameCard.js +108 -0
- package/dist/SignatureCard.d.ts +4 -0
- package/dist/SignatureCard.js +8 -0
- package/dist/SymbolPicker.d.ts +6 -0
- package/dist/SymbolPicker.js +108 -0
- package/dist/code-actions-model.d.ts +23 -0
- package/dist/code-actions-model.js +114 -0
- package/dist/code-actions.d.ts +30 -0
- package/dist/code-actions.js +326 -0
- package/dist/code-authors.d.ts +18 -0
- package/dist/code-authors.js +79 -0
- package/dist/code-vision-model.d.ts +21 -0
- package/dist/code-vision-model.js +183 -0
- package/dist/code-vision.d.ts +48 -0
- package/dist/code-vision.js +236 -0
- package/dist/completion-call.d.ts +24 -0
- package/dist/completion-call.js +110 -0
- package/dist/completion-model.d.ts +51 -0
- package/dist/completion-model.js +353 -0
- package/dist/completion.d.ts +43 -0
- package/dist/completion.js +463 -0
- package/dist/conflict-model.d.ts +15 -0
- package/dist/conflict-model.js +36 -0
- package/dist/context-menu.d.ts +10 -0
- package/dist/context-menu.js +55 -0
- package/dist/definition-link.d.ts +14 -0
- package/dist/definition-link.js +99 -0
- package/dist/diagnostics-model.d.ts +15 -0
- package/dist/diagnostics-model.js +83 -0
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +95 -0
- package/dist/editor-language.d.ts +65 -0
- package/dist/editor-language.js +170 -0
- package/dist/editor-react.css +43 -0
- package/dist/find-query.d.ts +26 -0
- package/dist/find-query.js +51 -0
- package/dist/folding.d.ts +16 -0
- package/dist/folding.js +95 -0
- package/dist/highlight-layers.d.ts +10 -0
- package/dist/highlight-layers.js +30 -0
- package/dist/highlights.d.ts +16 -0
- package/dist/highlights.js +102 -0
- package/dist/history.d.ts +8 -0
- package/dist/history.js +46 -0
- package/dist/holder.d.ts +5 -0
- package/dist/holder.js +19 -0
- package/dist/host-types.d.ts +45 -0
- package/dist/host-types.js +1 -0
- package/dist/hover-content.d.ts +45 -0
- package/dist/hover-content.js +198 -0
- package/dist/hover.d.ts +32 -0
- package/dist/hover.js +260 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +28 -0
- package/dist/inlay-hints.d.ts +13 -0
- package/dist/inlay-hints.js +71 -0
- package/dist/language-ids.d.ts +3 -0
- package/dist/language-ids.js +49 -0
- package/dist/line-edits.d.ts +8 -0
- package/dist/line-edits.js +39 -0
- package/dist/locales/en.json +171 -0
- package/dist/locales/nl.json +171 -0
- package/dist/models.d.ts +20 -0
- package/dist/models.js +20 -0
- package/dist/navigation-history.d.ts +16 -0
- package/dist/navigation-history.js +53 -0
- package/dist/navigation.d.ts +23 -0
- package/dist/navigation.js +155 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +3 -0
- package/dist/peek-model.d.ts +28 -0
- package/dist/peek-model.js +104 -0
- package/dist/peek.d.ts +30 -0
- package/dist/peek.js +215 -0
- package/dist/pick.d.ts +34 -0
- package/dist/pick.js +166 -0
- package/dist/popup-placement.d.ts +23 -0
- package/dist/popup-placement.js +34 -0
- package/dist/popups.d.ts +133 -0
- package/dist/popups.js +14 -0
- package/dist/project-language.d.ts +36 -0
- package/dist/project-language.js +190 -0
- package/dist/project-problems.d.ts +24 -0
- package/dist/project-problems.js +63 -0
- package/dist/proposal-model.d.ts +36 -0
- package/dist/proposal-model.js +197 -0
- package/dist/recent-choices.d.ts +10 -0
- package/dist/recent-choices.js +39 -0
- package/dist/refresher.d.ts +13 -0
- package/dist/refresher.js +50 -0
- package/dist/relative-time.d.ts +1 -0
- package/dist/relative-time.js +9 -0
- package/dist/rename-model.d.ts +25 -0
- package/dist/rename-model.js +93 -0
- package/dist/rename.d.ts +21 -0
- package/dist/rename.js +257 -0
- package/dist/rendering-context.d.ts +6 -0
- package/dist/rendering-context.js +5 -0
- package/dist/rendering.d.ts +6 -0
- package/dist/rendering.js +5 -0
- package/dist/review-group.d.ts +16 -0
- package/dist/review-group.js +54 -0
- package/dist/row-host.d.ts +42 -0
- package/dist/row-host.js +120 -0
- package/dist/selection-ranges.d.ts +7 -0
- package/dist/selection-ranges.js +26 -0
- package/dist/semantic-model.d.ts +4 -0
- package/dist/semantic-model.js +76 -0
- package/dist/semantic-tokens.d.ts +6 -0
- package/dist/semantic-tokens.js +38 -0
- package/dist/shortcut-keys.d.ts +91 -0
- package/dist/shortcut-keys.js +12 -0
- package/dist/signature-model.d.ts +18 -0
- package/dist/signature-model.js +33 -0
- package/dist/signature.d.ts +22 -0
- package/dist/signature.js +150 -0
- package/dist/snippet-session.d.ts +17 -0
- package/dist/snippet-session.js +110 -0
- package/dist/snippet.d.ts +11 -0
- package/dist/snippet.js +134 -0
- package/dist/symbol-links.d.ts +6 -0
- package/dist/symbol-links.js +50 -0
- package/dist/symbol-picker-model.d.ts +44 -0
- package/dist/symbol-picker-model.js +146 -0
- package/dist/symbol-picker.d.ts +12 -0
- package/dist/symbol-picker.js +76 -0
- package/dist/symbols.d.ts +11 -0
- package/dist/symbols.js +95 -0
- package/dist/testing.d.ts +57 -0
- package/dist/testing.js +156 -0
- package/dist/timers.d.ts +14 -0
- package/dist/timers.js +34 -0
- package/dist/use-problem-counts.d.ts +3 -0
- package/dist/use-problem-counts.js +20 -0
- package/dist/view-state.d.ts +21 -0
- package/dist/view-state.js +63 -0
- package/dist/word-diff.d.ts +1 -0
- package/dist/word-diff.js +32 -0
- package/dist/workspace-edit.d.ts +21 -0
- package/dist/workspace-edit.js +147 -0
- package/package.json +74 -2
- package/src/AnchoredPopup.tsx +51 -0
- package/src/AttributionCard.tsx +36 -0
- package/src/BaselineStatus.tsx +20 -0
- package/src/ChangeReview.tsx +43 -0
- package/src/CodeAuthorsCard.tsx +81 -0
- package/src/CompletionPopup.tsx +196 -0
- package/src/EditorContextMenu.tsx +67 -0
- package/src/EditorView.tsx +47 -0
- package/src/FindReplace.tsx +89 -0
- package/src/HoverCard.tsx +121 -0
- package/src/HoverSections.tsx +146 -0
- package/src/LanguagePopups.tsx +65 -0
- package/src/Markdown.tsx +11 -0
- package/src/PathText.tsx +18 -0
- package/src/PeekPanel.tsx +222 -0
- package/src/PickPopup.tsx +102 -0
- package/src/RenameCard.tsx +219 -0
- package/src/SignatureCard.tsx +42 -0
- package/src/SymbolPicker.tsx +219 -0
- package/src/code-actions-model.ts +146 -0
- package/src/code-actions.ts +384 -0
- package/src/code-authors.ts +104 -0
- package/src/code-vision-model.ts +230 -0
- package/src/code-vision.ts +269 -0
- package/src/completion-call.ts +139 -0
- package/src/completion-model.ts +440 -0
- package/src/completion.ts +500 -0
- package/src/conflict-model.ts +56 -0
- package/src/context-menu.ts +63 -0
- package/src/definition-link.ts +106 -0
- package/src/diagnostics-model.ts +102 -0
- package/src/diagnostics.ts +117 -0
- package/src/editor-language.ts +189 -0
- package/src/editor-react.css +43 -0
- package/src/find-query.ts +74 -0
- package/src/folding.ts +110 -0
- package/src/highlight-layers.ts +37 -0
- package/src/highlights.ts +123 -0
- package/src/history.ts +56 -0
- package/src/holder.ts +19 -0
- package/src/host-types.ts +52 -0
- package/src/hover-content.ts +264 -0
- package/src/hover.ts +280 -0
- package/src/index.ts +63 -0
- package/src/inlay-hints.ts +85 -0
- package/src/language-ids.ts +54 -0
- package/src/line-edits.ts +48 -0
- package/src/locales/en.json +171 -0
- package/src/locales/nl.json +171 -0
- package/src/models.ts +157 -0
- package/src/navigation-history.ts +69 -0
- package/src/navigation.ts +168 -0
- package/src/paths.ts +3 -0
- package/src/peek-model.ts +147 -0
- package/src/peek.ts +247 -0
- package/src/pick.ts +197 -0
- package/src/popup-placement.ts +64 -0
- package/src/popups.ts +204 -0
- package/src/project-language.ts +223 -0
- package/src/project-problems.ts +87 -0
- package/src/proposal-model.ts +259 -0
- package/src/recent-choices.ts +48 -0
- package/src/refresher.ts +56 -0
- package/src/relative-time.ts +10 -0
- package/src/rename-model.ts +130 -0
- package/src/rename.ts +290 -0
- package/src/rendering-context.ts +12 -0
- package/src/rendering.tsx +6 -0
- package/src/review-group.ts +70 -0
- package/src/row-host.ts +158 -0
- package/src/selection-ranges.ts +32 -0
- package/src/semantic-model.ts +80 -0
- package/src/semantic-tokens.ts +46 -0
- package/src/shortcut-keys.ts +21 -0
- package/src/signature-model.ts +49 -0
- package/src/signature.ts +164 -0
- package/src/snippet-session.ts +122 -0
- package/src/snippet.ts +147 -0
- package/src/symbol-links.ts +72 -0
- package/src/symbol-picker-model.ts +198 -0
- package/src/symbol-picker.ts +85 -0
- package/src/symbols.ts +110 -0
- package/src/testing.ts +240 -0
- package/src/timers.ts +45 -0
- package/src/use-problem-counts.ts +25 -0
- package/src/view-state.ts +88 -0
- package/src/word-diff.ts +35 -0
- package/src/workspace-edit.ts +194 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { AtSign, Hash } from 'lucide-react';
|
|
6
|
+
import { Icon } from '@adecore/ui';
|
|
7
|
+
import { formatShortcut } from '@adecore/ui';
|
|
8
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
9
|
+
import { basenameOf } from './paths.js';
|
|
10
|
+
import { fileUriToPath } from '@adecore/lsp';
|
|
11
|
+
import { filterEntries, groupEntries, letterOfKind, modeOf, toneOfKind } from './symbol-picker-model.js';
|
|
12
|
+
const TONES = {
|
|
13
|
+
callable: 'bg-accent-soft text-accent',
|
|
14
|
+
value: 'bg-status-running/15 text-status-running',
|
|
15
|
+
type: 'bg-status-idle/15 text-status-idle',
|
|
16
|
+
other: 'bg-surface-hover text-text-muted'
|
|
17
|
+
};
|
|
18
|
+
const SEARCH_DELAY_MS = 200;
|
|
19
|
+
function Badge({ kind }) {
|
|
20
|
+
return (_jsx("span", { className: `grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONES[toneOfKind(kind)]}`, children: letterOfKind(kind) }));
|
|
21
|
+
}
|
|
22
|
+
/*
|
|
23
|
+
* The picker of go to symbol, over the editor: an input, the symbols of the file in groups by kind, and the
|
|
24
|
+
* keys. The input keeps the focus; Enter goes, Escape and the focus leaving close it.
|
|
25
|
+
*/
|
|
26
|
+
export function SymbolPicker({ language, view }) {
|
|
27
|
+
const { t } = useTranslation('editor');
|
|
28
|
+
const [input, setInput] = useState('');
|
|
29
|
+
const [active, setActive] = useState(0);
|
|
30
|
+
const [found, setFound] = useState({ text: '', entries: [] });
|
|
31
|
+
const activeRow = useRef(null);
|
|
32
|
+
const mode = modeOf(input);
|
|
33
|
+
const filterText = mode.mode === 'symbol' ? mode.text : '';
|
|
34
|
+
const searchText = mode.mode === 'workspace' ? mode.text : '';
|
|
35
|
+
const groups = useMemo(() => groupEntries(filterEntries(view.entries, filterText)), [view.entries, filterText]);
|
|
36
|
+
const flat = groups.flatMap((group) => group.entries);
|
|
37
|
+
const places = mode.mode === 'workspace' && found.text === mode.text ? found.entries : [];
|
|
38
|
+
const count = mode.mode === 'workspace' ? places.length : mode.mode === 'line' ? (mode.line === null ? 0 : 1) : flat.length;
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (searchText === '') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const text = searchText;
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
language.symbolPicker
|
|
47
|
+
.search(text, controller.signal)
|
|
48
|
+
.then((entries) => !controller.signal.aborted && setFound({ text, entries }))
|
|
49
|
+
.catch(() => undefined);
|
|
50
|
+
}, SEARCH_DELAY_MS);
|
|
51
|
+
return () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
controller.abort();
|
|
54
|
+
};
|
|
55
|
+
}, [language, searchText]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
activeRow.current?.scrollIntoView({ block: 'nearest' });
|
|
58
|
+
}, [active, input]);
|
|
59
|
+
function choose(index) {
|
|
60
|
+
if (mode.mode === 'line') {
|
|
61
|
+
if (mode.line !== null) {
|
|
62
|
+
language.symbolPicker.goToLine(mode.line);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (mode.mode === 'workspace') {
|
|
66
|
+
const place = places[index];
|
|
67
|
+
if (place !== undefined) {
|
|
68
|
+
language.symbolPicker.goToWorkspace(place);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (flat[index] !== undefined) {
|
|
72
|
+
language.symbolPicker.goTo(flat[index]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function onKeyDown(event) {
|
|
76
|
+
if (event.key === 'Escape') {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
language.symbolPicker.close();
|
|
80
|
+
}
|
|
81
|
+
else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
choose(active);
|
|
85
|
+
}
|
|
86
|
+
else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
setActive(Math.max(0, Math.min(Math.max(0, count - 1), active + (event.key === 'ArrowDown' ? 1 : -1))));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let offset = 0;
|
|
92
|
+
return createPortal(_jsxs("div", { className: "fixed top-[12vh] left-1/2 z-(--z-popup) flex w-[560px] max-w-[calc(100vw-16px)] -translate-x-1/2 flex-col overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)", onPointerDown: (event) => event.target !== event.currentTarget && event.target.tagName !== 'INPUT' && event.preventDefault(), children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3 py-2", children: [_jsx(Icon, { icon: mode.mode === 'workspace' ? Hash : AtSign, size: 14, className: "shrink-0 text-text-muted" }), _jsx("input", { autoFocus: true, value: input, spellCheck: false, autoComplete: "off", placeholder: t('language.symbols.placeholder', { file: view.file }), className: "min-w-0 flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-faint", onChange: (event) => {
|
|
93
|
+
setInput(event.target.value);
|
|
94
|
+
setActive(0);
|
|
95
|
+
}, onKeyDown: onKeyDown, onBlur: () => language.symbolPicker.close() }), _jsx("span", { className: "shrink-0 text-xs text-text-faint", children: language.shortcuts.goToSymbol === null ? '' : formatShortcut(language.shortcuts.goToSymbol, language.apple) })] }), _jsxs("div", { role: "listbox", className: "max-h-[320px] overflow-y-auto p-1", children: [mode.mode === 'line' && (_jsx("div", { className: "px-2 py-1.5 text-xs text-text-muted", children: mode.line === null ? t('language.symbols.lineHint') : t('language.symbols.goToLine', { line: formatNumber(mode.line) }) })), mode.mode === 'workspace' &&
|
|
96
|
+
places.map((place, index) => {
|
|
97
|
+
const path = fileUriToPath(place.uri);
|
|
98
|
+
return (_jsxs("button", { ref: index === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": index === active, "data-active": index === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(index), children: [_jsx(Badge, { kind: place.kind }), _jsx("span", { className: "shrink-0", children: place.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: place.container }), _jsxs("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: [path === null ? '' : basenameOf(path), ":", formatNumber(place.line + 1)] })] }, place.id));
|
|
99
|
+
}), mode.mode === 'symbol' &&
|
|
100
|
+
groups.map((group) => {
|
|
101
|
+
const start = offset;
|
|
102
|
+
offset += group.entries.length;
|
|
103
|
+
return (_jsxs("div", { role: "group", children: [_jsx("div", { className: "px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint", children: t(`language.symbols.groups.${group.group}`) }), group.entries.map((entry, index) => {
|
|
104
|
+
const position = start + index;
|
|
105
|
+
return (_jsxs("button", { ref: position === active ? activeRow : undefined, type: "button", role: "option", "aria-selected": position === active, "data-active": position === active, className: "flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row", onClick: () => choose(position), children: [_jsx(Badge, { kind: entry.kind }), _jsx("span", { className: "shrink-0", children: entry.name }), _jsx("span", { className: "min-w-0 truncate font-mono text-text-faint", children: entry.detail }), entry.container !== '' && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: entry.container }), _jsx("span", { className: "ml-auto shrink-0 font-mono text-text-faint", children: formatNumber(entry.line + 1) })] }, entry.id));
|
|
106
|
+
})] }, group.group));
|
|
107
|
+
}), mode.mode === 'symbol' && flat.length === 0 && (_jsx("div", { className: "px-2 py-3 text-center text-xs text-text-muted", children: t('language.symbols.none') })), mode.mode === 'workspace' && places.length === 0 && (_jsx("div", { className: "px-2 py-3 text-center text-xs text-text-muted", children: mode.text === '' ? t('language.symbols.workspaceHint') : t('language.symbols.none') }))] }), _jsxs("div", { className: "flex items-center gap-3 border-t border-border px-3 py-1.5 text-xs whitespace-nowrap text-text-faint", children: [_jsxs("span", { children: ["\u2191\u2193 ", t('language.symbols.navigate')] }), _jsxs("span", { children: ["\u21B5 ", t('language.symbols.open')] }), _jsxs("button", { type: "button", className: "ml-auto hover:text-text", onClick: () => setInput('#'), children: ["# ", t('language.symbols.workspace')] }), _jsxs("button", { type: "button", className: "hover:text-text", onClick: () => setInput(':'), children: [": ", t('language.symbols.line')] })] })] }), document.body);
|
|
108
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CodeAction, type Command, type Diagnostic, type Range, type WorkspaceEdit } from '@adecore/lsp';
|
|
2
|
+
export type ActionGroup = 'quickfix' | 'rewrite' | 'extract' | 'inline' | 'move' | 'refactor' | 'source' | 'other';
|
|
3
|
+
export declare const ACTION_GROUPS: readonly ActionGroup[];
|
|
4
|
+
export declare const REFACTOR_GROUPS: readonly ActionGroup[];
|
|
5
|
+
export interface ActionEntry {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly action: CodeAction;
|
|
8
|
+
readonly group: ActionGroup;
|
|
9
|
+
}
|
|
10
|
+
export declare function groupOf(kind: string | undefined): ActionGroup;
|
|
11
|
+
export declare function actionsOf(result: readonly (CodeAction | Command)[] | null, order?: readonly ActionGroup[]): ActionEntry[];
|
|
12
|
+
export declare function mergeEntries(lists: readonly (readonly ActionEntry[])[]): ActionEntry[];
|
|
13
|
+
export declare function isHint(entry: ActionEntry): boolean;
|
|
14
|
+
export declare function fixableOnLine(diagnostics: readonly Diagnostic[], line: number): Diagnostic[];
|
|
15
|
+
export declare function diagnosticsAt(diagnostics: readonly Diagnostic[], range: Range): Diagnostic[];
|
|
16
|
+
export interface EditPreview {
|
|
17
|
+
readonly removed: readonly string[];
|
|
18
|
+
readonly added: readonly string[];
|
|
19
|
+
readonly hiddenLines: number;
|
|
20
|
+
readonly otherFiles: number;
|
|
21
|
+
readonly moves: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditPreview | null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { applyTextEdits } from '@adecore/lsp';
|
|
2
|
+
import { comparePositions } from './diagnostics-model.js';
|
|
3
|
+
import { entriesOf, renamesOf } from './workspace-edit.js';
|
|
4
|
+
/* The order of the list under the caret: the fixes and the rewrites that are one keystroke away first, the refactorings after them. */
|
|
5
|
+
export const ACTION_GROUPS = ['quickfix', 'rewrite', 'extract', 'inline', 'move', 'refactor', 'source', 'other'];
|
|
6
|
+
/* The order of Refactor This: what changes the structure of the code first, the rewrites after it. */
|
|
7
|
+
export const REFACTOR_GROUPS = ['extract', 'inline', 'move', 'rewrite', 'refactor'];
|
|
8
|
+
/* The kinds nest by dots: `refactor.extract.function` is an extraction and `source.organizeImports` a source action. */
|
|
9
|
+
export function groupOf(kind) {
|
|
10
|
+
if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
|
|
11
|
+
return 'quickfix';
|
|
12
|
+
}
|
|
13
|
+
for (const group of ['extract', 'inline', 'move', 'rewrite']) {
|
|
14
|
+
if (kind === `refactor.${group}` || kind?.startsWith(`refactor.${group}.`)) {
|
|
15
|
+
return group;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (kind === 'refactor' || kind?.startsWith('refactor.')) {
|
|
19
|
+
return 'refactor';
|
|
20
|
+
}
|
|
21
|
+
if (kind === 'source' || kind?.startsWith('source.')) {
|
|
22
|
+
return 'source';
|
|
23
|
+
}
|
|
24
|
+
return 'other';
|
|
25
|
+
}
|
|
26
|
+
/*
|
|
27
|
+
* What a server answered, as the rows of a list. A bare command becomes an action of its own, one a server
|
|
28
|
+
* marked disabled is left out, and each group lists the preferred actions first, in the order the server gave.
|
|
29
|
+
*/
|
|
30
|
+
export function actionsOf(result, order = ACTION_GROUPS) {
|
|
31
|
+
const entries = (result ?? [])
|
|
32
|
+
.map((item) => (typeof item.command === 'string' ? { title: item.title, command: item } : item))
|
|
33
|
+
.filter((action) => action.disabled === undefined)
|
|
34
|
+
.map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
|
|
35
|
+
return order.flatMap((group) => {
|
|
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
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/* 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
|
+
export function mergeEntries(lists) {
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const unique = lists.flat().filter((entry) => {
|
|
44
|
+
const key = `${entry.action.kind ?? ''}\0${entry.action.title}`;
|
|
45
|
+
if (seen.has(key)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
seen.add(key);
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
return ACTION_GROUPS.flatMap((group) => {
|
|
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
|
+
}
|
|
56
|
+
/* Whether the lightbulb offers it: a source action such as organizing imports is a command of its own and never a hint. */
|
|
57
|
+
export function isHint(entry) {
|
|
58
|
+
return entry.group !== 'source';
|
|
59
|
+
}
|
|
60
|
+
/* The problems of a line that are worth a fix: errors and warnings, errors first, in the order of the line. */
|
|
61
|
+
export function fixableOnLine(diagnostics, line) {
|
|
62
|
+
return diagnostics
|
|
63
|
+
.filter((diagnostic) => (diagnostic.severity ?? 1) <= 2 && diagnostic.range.start.line <= line && diagnostic.range.end.line >= line)
|
|
64
|
+
.sort((left, right) => (left.severity ?? 1) - (right.severity ?? 1) || comparePositions(left.range.start, right.range.start));
|
|
65
|
+
}
|
|
66
|
+
function touches(range, target) {
|
|
67
|
+
return comparePositions(range.start, target.end) <= 0 && comparePositions(target.start, range.end) <= 0;
|
|
68
|
+
}
|
|
69
|
+
/* The problems an action may be about: the ones that overlap or touch the range it was asked for. */
|
|
70
|
+
export function diagnosticsAt(diagnostics, range) {
|
|
71
|
+
return diagnostics.filter((diagnostic) => touches(diagnostic.range, range));
|
|
72
|
+
}
|
|
73
|
+
const PREVIEW_LINES = 3;
|
|
74
|
+
function linesOf(text) {
|
|
75
|
+
return text.split(/\r\n|\r|\n/);
|
|
76
|
+
}
|
|
77
|
+
/* Null when the edit changes nothing, or creates or deletes a file, which a preview of lines cannot show. */
|
|
78
|
+
export function previewOf(text, edit, uri) {
|
|
79
|
+
const entries = entriesOf(edit);
|
|
80
|
+
if (entries === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const own = entries.get(uri);
|
|
84
|
+
const moves = renamesOf(edit).length;
|
|
85
|
+
const otherFiles = [...entries.keys()].filter((key) => key !== uri).length;
|
|
86
|
+
let removed = [];
|
|
87
|
+
let added = [];
|
|
88
|
+
if (own !== undefined) {
|
|
89
|
+
let after;
|
|
90
|
+
try {
|
|
91
|
+
after = applyTextEdits(text, own.flat());
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const before = linesOf(text);
|
|
97
|
+
const next = linesOf(after);
|
|
98
|
+
let head = 0;
|
|
99
|
+
while (head < before.length && head < next.length && before[head] === next[head]) {
|
|
100
|
+
head++;
|
|
101
|
+
}
|
|
102
|
+
let tail = 0;
|
|
103
|
+
while (tail < before.length - head && tail < next.length - head && before[before.length - 1 - tail] === next[next.length - 1 - tail]) {
|
|
104
|
+
tail++;
|
|
105
|
+
}
|
|
106
|
+
removed = before.slice(head, before.length - tail);
|
|
107
|
+
added = next.slice(head, next.length - tail);
|
|
108
|
+
}
|
|
109
|
+
if (removed.length === 0 && added.length === 0 && otherFiles === 0 && moves === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const hiddenLines = Math.max(0, removed.length - PREVIEW_LINES) + Math.max(0, added.length - PREVIEW_LINES);
|
|
113
|
+
return { removed: removed.slice(0, PREVIEW_LINES), added: added.slice(0, PREVIEW_LINES), hiddenLines, otherFiles, moves };
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ActionEntry } from './code-actions-model.ts';
|
|
2
|
+
import type { Problem } from './diagnostics-model.ts';
|
|
3
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
4
|
+
import { type Timers } from './timers.ts';
|
|
5
|
+
export declare class CodeActionsFeature {
|
|
6
|
+
private tell;
|
|
7
|
+
private readonly language;
|
|
8
|
+
private readonly refresher;
|
|
9
|
+
private asking;
|
|
10
|
+
private readonly resolved;
|
|
11
|
+
constructor(language: EditorLanguage, timers?: Timers);
|
|
12
|
+
get supported(): boolean;
|
|
13
|
+
open(): Promise<void>;
|
|
14
|
+
refactorThis(): Promise<void>;
|
|
15
|
+
quickFixFor(problem: Problem): Promise<void>;
|
|
16
|
+
organizeImports(): Promise<void>;
|
|
17
|
+
formatDocument(): Promise<void>;
|
|
18
|
+
list(only: readonly string[]): Promise<ActionEntry[] | null>;
|
|
19
|
+
private ask;
|
|
20
|
+
private request;
|
|
21
|
+
private requestWithLine;
|
|
22
|
+
private hint;
|
|
23
|
+
private resolve;
|
|
24
|
+
private preview;
|
|
25
|
+
private refusalOf;
|
|
26
|
+
apply(entry: ActionEntry): Promise<void>;
|
|
27
|
+
private startRename;
|
|
28
|
+
private fail;
|
|
29
|
+
private say;
|
|
30
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { ErrorCodes, LspError, StaleResultError } from '@adecore/lsp';
|
|
2
|
+
import { ACTION_GROUPS, REFACTOR_GROUPS, actionsOf, diagnosticsAt, fixableOnLine, isHint, mergeEntries, previewOf } from './code-actions-model.js';
|
|
3
|
+
import { Refresher } from './refresher.js';
|
|
4
|
+
import { isShortcut } from './shortcut-keys.js';
|
|
5
|
+
import { realTimers } from './timers.js';
|
|
6
|
+
const METHOD = 'textDocument/codeAction';
|
|
7
|
+
const PAUSE_MS = 250;
|
|
8
|
+
const INVOKED = 1;
|
|
9
|
+
const AUTOMATIC = 2;
|
|
10
|
+
const TOAST_ID = 'language-action';
|
|
11
|
+
/* How many problems of the line are asked for their fixes besides the caret's own request. */
|
|
12
|
+
const LINE_PROBLEMS = 3;
|
|
13
|
+
/* The command of the PHP server that asks the client to rename the name a refactor just wrote; the server answers it with nothing. */
|
|
14
|
+
const INLINE_RENAME_COMMAND = 'php.rename';
|
|
15
|
+
/* A server that would not make the action, with its reason. */
|
|
16
|
+
class ActionRefused extends Error {
|
|
17
|
+
}
|
|
18
|
+
function isRefusal(error) {
|
|
19
|
+
return error instanceof LspError && error.code === ErrorCodes.InternalError;
|
|
20
|
+
}
|
|
21
|
+
/*
|
|
22
|
+
* What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
|
|
23
|
+
* that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
|
|
24
|
+
* shown under it, the Quick fix of a problem's card, and the commands that organize imports and format the file.
|
|
25
|
+
* An action is resolved before it is applied, since a server fills in its edit only when asked for it.
|
|
26
|
+
*/
|
|
27
|
+
export class CodeActionsFeature {
|
|
28
|
+
tell(kind, title) {
|
|
29
|
+
this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
|
|
30
|
+
}
|
|
31
|
+
language;
|
|
32
|
+
refresher;
|
|
33
|
+
/* While a list is being asked for, the lightbulb waits: both are the same request and the newer one would cancel the older. */
|
|
34
|
+
asking = false;
|
|
35
|
+
resolved = new Map();
|
|
36
|
+
constructor(language, timers = realTimers) {
|
|
37
|
+
this.language = language;
|
|
38
|
+
const { editor } = language;
|
|
39
|
+
this.refresher = new Refresher((signal) => this.hint(signal), PAUSE_MS, timers);
|
|
40
|
+
const providers = language.onProvidersChanged(() => {
|
|
41
|
+
this.refresher.later();
|
|
42
|
+
});
|
|
43
|
+
const offs = [
|
|
44
|
+
editor.onCaret(() => {
|
|
45
|
+
editor.setGutterAction(null);
|
|
46
|
+
this.refresher.later();
|
|
47
|
+
}),
|
|
48
|
+
editor.onTextChange(() => editor.setGutterAction(null)),
|
|
49
|
+
editor.onGutterAction(() => void this.open()),
|
|
50
|
+
editor.onKeyDown((event) => {
|
|
51
|
+
if (isShortcut(this.language.shortcuts.codeActions, event)) {
|
|
52
|
+
void this.open();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (isShortcut(this.language.shortcuts.refactorThis, event)) {
|
|
56
|
+
void this.refactorThis();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}),
|
|
61
|
+
language.diagnostics.onChange(() => this.refresher.later()),
|
|
62
|
+
providers
|
|
63
|
+
];
|
|
64
|
+
language.onDispose(() => {
|
|
65
|
+
for (const off of offs) {
|
|
66
|
+
off();
|
|
67
|
+
}
|
|
68
|
+
this.refresher.dispose();
|
|
69
|
+
editor.setGutterAction(null);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
get supported() {
|
|
73
|
+
return this.language.project.service.supports(METHOD, this.language.uri);
|
|
74
|
+
}
|
|
75
|
+
/* The list under the caret, or under the selection's start. */
|
|
76
|
+
open() {
|
|
77
|
+
const range = this.language.editor.getSelection();
|
|
78
|
+
return this.ask({ range, anchor: range.start, withLine: true });
|
|
79
|
+
}
|
|
80
|
+
/* Refactor This: only the refactorings, grouped by what they do, under the caret or the selection. */
|
|
81
|
+
refactorThis() {
|
|
82
|
+
const range = this.language.editor.getSelection();
|
|
83
|
+
return this.ask({ range, only: ['refactor'], anchor: range.start, groups: REFACTOR_GROUPS });
|
|
84
|
+
}
|
|
85
|
+
/* The quick fixes of one problem, under it, as the Quick fix button of its card asks. */
|
|
86
|
+
quickFixFor(problem) {
|
|
87
|
+
const { range } = problem.diagnostic;
|
|
88
|
+
return this.ask({ range, only: ['quickfix'], anchor: range.start, diagnostics: [problem.diagnostic] });
|
|
89
|
+
}
|
|
90
|
+
async organizeImports() {
|
|
91
|
+
const { editor } = this.language;
|
|
92
|
+
const everything = { start: { line: 0, character: 0 }, end: editor.positionAt(Number.MAX_SAFE_INTEGER) };
|
|
93
|
+
const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
|
|
94
|
+
const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
|
|
95
|
+
if (entry === undefined) {
|
|
96
|
+
this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await this.apply(entry);
|
|
100
|
+
}
|
|
101
|
+
async formatDocument() {
|
|
102
|
+
const { editor, project, uri } = this.language;
|
|
103
|
+
if (!project.service.supports('textDocument/formatting', uri)) {
|
|
104
|
+
this.tell('error', this.say('noFormatter'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
|
|
109
|
+
if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
|
|
110
|
+
this.tell('error', this.say('readOnly'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.fail(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
|
|
118
|
+
list(only) {
|
|
119
|
+
return this.supported
|
|
120
|
+
? this.request(this.language.editor.getSelection(), only, AUTOMATIC, undefined, undefined, only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS)
|
|
121
|
+
: Promise.resolve(null);
|
|
122
|
+
}
|
|
123
|
+
async ask(options) {
|
|
124
|
+
if (!this.supported) {
|
|
125
|
+
this.tell('error', this.say('unavailable'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// The press on a card or the gutter may have taken the focus, and the list answers to the editor's keys.
|
|
129
|
+
this.language.editor.focus();
|
|
130
|
+
this.asking = true;
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = options.withLine
|
|
134
|
+
? await this.requestWithLine(options.range, INVOKED)
|
|
135
|
+
: await this.request(options.range, options.only, INVOKED, undefined, options.diagnostics, options.groups);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
this.asking = false;
|
|
139
|
+
}
|
|
140
|
+
if (entries === null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (entries.length === 0) {
|
|
144
|
+
this.tell('success', this.say('none'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
148
|
+
const groups = (options.groups ?? ACTION_GROUPS).flatMap((group) => {
|
|
149
|
+
const rows = entries.filter((entry) => entry.group === group).map((entry) => ({ id: entry.id, label: entry.action.title, detail: '' }));
|
|
150
|
+
return rows.length === 0 ? [] : [{ title: this.say(`groups.${group}`), rows }];
|
|
151
|
+
});
|
|
152
|
+
this.language.completion.close();
|
|
153
|
+
this.language.pick.open({
|
|
154
|
+
anchor: options.anchor,
|
|
155
|
+
groups,
|
|
156
|
+
accept: (id) => {
|
|
157
|
+
const entry = byId.get(id);
|
|
158
|
+
if (entry !== undefined) {
|
|
159
|
+
void this.apply(entry);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
attempt: (id) => this.refusalOf(byId.get(id)),
|
|
163
|
+
preview: (id, signal) => this.preview(byId.get(id), signal)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/* Asks the servers; null when they could not answer, with the reason said aloud for a request a person made. */
|
|
167
|
+
async request(range, only, triggerKind, signal, about, order) {
|
|
168
|
+
const { project, uri, diagnostics } = this.language;
|
|
169
|
+
try {
|
|
170
|
+
const context = {
|
|
171
|
+
diagnostics: [
|
|
172
|
+
...(about ??
|
|
173
|
+
diagnosticsAt(diagnostics.problems.map((problem) => problem.diagnostic), range))
|
|
174
|
+
],
|
|
175
|
+
...(only ? { only: [...only] } : {}),
|
|
176
|
+
triggerKind: triggerKind
|
|
177
|
+
};
|
|
178
|
+
const entries = actionsOf(await project.service.codeActions(uri, range, context, { signal }), order);
|
|
179
|
+
// A server may answer with more than the kinds asked for.
|
|
180
|
+
return only ? entries.filter((entry) => only.some((kind) => entry.action.kind === kind || entry.action.kind?.startsWith(`${kind}.`))) : entries;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (triggerKind === INVOKED) {
|
|
184
|
+
this.fail(error);
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/*
|
|
190
|
+
* What the servers offer at the range, and when no problem touches it, the quick fixes of the problems
|
|
191
|
+
* elsewhere on its line too, since the fix of an error is wanted from anywhere on the line it is on.
|
|
192
|
+
*/
|
|
193
|
+
async requestWithLine(range, triggerKind, signal) {
|
|
194
|
+
const entries = await this.request(range, undefined, triggerKind, signal);
|
|
195
|
+
if (entries === null || range.start.line !== range.end.line) {
|
|
196
|
+
return entries;
|
|
197
|
+
}
|
|
198
|
+
const all = this.language.diagnostics.problems.map((problem) => problem.diagnostic);
|
|
199
|
+
if (diagnosticsAt(all, range).length > 0) {
|
|
200
|
+
return entries;
|
|
201
|
+
}
|
|
202
|
+
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'], triggerKind, signal, [diagnostic])));
|
|
204
|
+
return mergeEntries([entries, ...fixes.map((list) => list ?? [])]);
|
|
205
|
+
}
|
|
206
|
+
async hint(signal) {
|
|
207
|
+
const { editor } = this.language;
|
|
208
|
+
if (this.asking || !this.supported) {
|
|
209
|
+
editor.setGutterAction(null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const range = editor.getSelection();
|
|
213
|
+
const entries = await this.requestWithLine(range, AUTOMATIC, signal);
|
|
214
|
+
if (signal.aborted || this.asking) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: this.say('show') } : null);
|
|
218
|
+
}
|
|
219
|
+
async resolve(entry, signal) {
|
|
220
|
+
const known = this.resolved.get(entry);
|
|
221
|
+
if (known !== undefined) {
|
|
222
|
+
return known;
|
|
223
|
+
}
|
|
224
|
+
const { project, uri } = this.language;
|
|
225
|
+
const { action } = entry;
|
|
226
|
+
let result = action;
|
|
227
|
+
if (action.edit === undefined && project.service.providerOptions(METHOD, uri)?.resolveProvider === true) {
|
|
228
|
+
try {
|
|
229
|
+
result = await project.service.resolveCodeAction(uri, action, { signal });
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (isRefusal(error)) {
|
|
233
|
+
throw new ActionRefused(error.message);
|
|
234
|
+
}
|
|
235
|
+
// An action the server cannot resolve is still tried as it is, unless the text moved under it.
|
|
236
|
+
if (error instanceof StaleResultError) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.resolved.set(entry, result);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
async preview(entry, signal) {
|
|
245
|
+
if (entry === undefined) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
let action;
|
|
249
|
+
try {
|
|
250
|
+
action = await this.resolve(entry, signal);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof ActionRefused) {
|
|
254
|
+
return { removed: [], added: [], note: null, refusal: error.message };
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
const { editor, uri } = this.language;
|
|
259
|
+
const found = action.edit === undefined ? null : previewOf(editor.getText(), action.edit, uri);
|
|
260
|
+
if (found === null) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const notes = [
|
|
264
|
+
...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
|
|
265
|
+
...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
|
|
266
|
+
...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
|
|
267
|
+
];
|
|
268
|
+
return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
|
|
269
|
+
}
|
|
270
|
+
/* Why a server would not make the action, or null when it will, which resolves it for `apply` to use. */
|
|
271
|
+
async refusalOf(entry) {
|
|
272
|
+
if (entry === undefined) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await this.resolve(entry);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return error instanceof ActionRefused ? error.message : null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/* Resolves the action, makes its edit as one undo step, and then runs its command. */
|
|
284
|
+
async apply(entry) {
|
|
285
|
+
const { editor, project, uri } = this.language;
|
|
286
|
+
try {
|
|
287
|
+
const action = await this.resolve(entry);
|
|
288
|
+
if (action.edit !== undefined) {
|
|
289
|
+
const result = await project.applyWorkspaceEdit(action.edit);
|
|
290
|
+
if (!result.applied) {
|
|
291
|
+
this.tell('error', this.say('failed', { message: result.failureReason ?? this.say('refused') }));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (action.command?.command === INLINE_RENAME_COMMAND) {
|
|
296
|
+
await this.startRename(action.command);
|
|
297
|
+
}
|
|
298
|
+
else if (action.command !== undefined) {
|
|
299
|
+
await project.service.executeCommand(uri, action.command);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.fail(error);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
this.resolved.delete(entry);
|
|
307
|
+
editor.focus();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/* The name a refactor wrote is offered for a rename right where it stands, the way the platform does after an extraction. */
|
|
311
|
+
async startRename(command) {
|
|
312
|
+
const target = (command.arguments?.[0] ?? null);
|
|
313
|
+
if (target?.position !== undefined && target.textDocument?.uri === this.language.uri) {
|
|
314
|
+
this.language.editor.setCaret(target.position);
|
|
315
|
+
await this.language.rename.start();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
fail(error) {
|
|
319
|
+
if (!(error instanceof StaleResultError)) {
|
|
320
|
+
this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
say(key, options) {
|
|
324
|
+
return this.language.project.i18n.t(`editor:language.actions.${key}`, options);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { i18n } from 'i18next';
|
|
2
|
+
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
3
|
+
export declare const UNCOMMITTED = -1;
|
|
4
|
+
export declare function mapBlame(blame: GitBlameResult, base: string, current: readonly string[]): Int32Array | null;
|
|
5
|
+
export interface CodeAuthor {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly email: string;
|
|
8
|
+
readonly lines: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CodeAuthorship {
|
|
11
|
+
readonly authors: readonly CodeAuthor[];
|
|
12
|
+
readonly uncommittedLines: number;
|
|
13
|
+
readonly modified: boolean;
|
|
14
|
+
readonly latest: GitBlameCommit | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function shortName(name: string): string;
|
|
17
|
+
export declare function authorshipOf(commits: readonly GitBlameCommit[], mapped: Int32Array, lines: readonly string[], from: number, to: number): CodeAuthorship;
|
|
18
|
+
export declare function authorsText(authorship: CodeAuthorship, i18n: i18n): string;
|