@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,384 @@
|
|
|
1
|
+
import { ErrorCodes, LspError, StaleResultError, type CodeAction, type Command, type Diagnostic, type Position } from '@adecore/lsp';
|
|
2
|
+
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
|
+
import {
|
|
4
|
+
ACTION_GROUPS,
|
|
5
|
+
REFACTOR_GROUPS,
|
|
6
|
+
actionsOf,
|
|
7
|
+
diagnosticsAt,
|
|
8
|
+
fixableOnLine,
|
|
9
|
+
isHint,
|
|
10
|
+
mergeEntries,
|
|
11
|
+
previewOf,
|
|
12
|
+
type ActionEntry,
|
|
13
|
+
type ActionGroup
|
|
14
|
+
} from './code-actions-model.ts';
|
|
15
|
+
import type { Problem } from './diagnostics-model.ts';
|
|
16
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
17
|
+
import type { PickGroup, PickPreview } from './popups.ts';
|
|
18
|
+
import { Refresher } from './refresher.ts';
|
|
19
|
+
import { isShortcut } from './shortcut-keys.ts';
|
|
20
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
21
|
+
|
|
22
|
+
const METHOD = 'textDocument/codeAction';
|
|
23
|
+
const PAUSE_MS = 250;
|
|
24
|
+
const INVOKED = 1;
|
|
25
|
+
const AUTOMATIC = 2;
|
|
26
|
+
const TOAST_ID = 'language-action';
|
|
27
|
+
/* How many problems of the line are asked for their fixes besides the caret's own request. */
|
|
28
|
+
const LINE_PROBLEMS = 3;
|
|
29
|
+
/* The command of the PHP server that asks the client to rename the name a refactor just wrote; the server answers it with nothing. */
|
|
30
|
+
const INLINE_RENAME_COMMAND = 'php.rename';
|
|
31
|
+
|
|
32
|
+
/* A server that would not make the action, with its reason. */
|
|
33
|
+
class ActionRefused extends Error {}
|
|
34
|
+
|
|
35
|
+
function isRefusal(error: unknown): boolean {
|
|
36
|
+
return error instanceof LspError && error.code === ErrorCodes.InternalError;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface AskOptions {
|
|
40
|
+
readonly range: EditorRange;
|
|
41
|
+
readonly only?: readonly string[];
|
|
42
|
+
readonly anchor: EditorPosition;
|
|
43
|
+
/* The problems the actions are about; the ones touching the range when absent. */
|
|
44
|
+
readonly diagnostics?: readonly Diagnostic[];
|
|
45
|
+
/* Also the fixes of a problem on the line when none touches the range. */
|
|
46
|
+
readonly withLine?: boolean;
|
|
47
|
+
/* The groups the list shows, in order; the quick fix list's own by default. */
|
|
48
|
+
readonly groups?: readonly ActionGroup[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
|
|
53
|
+
* that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
|
|
54
|
+
* shown under it, the Quick fix of a problem's card, and the commands that organize imports and format the file.
|
|
55
|
+
* An action is resolved before it is applied, since a server fills in its edit only when asked for it.
|
|
56
|
+
*/
|
|
57
|
+
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
|
+
private readonly language: EditorLanguage;
|
|
63
|
+
private readonly refresher: Refresher;
|
|
64
|
+
/* While a list is being asked for, the lightbulb waits: both are the same request and the newer one would cancel the older. */
|
|
65
|
+
private asking = false;
|
|
66
|
+
private readonly resolved = new Map<ActionEntry, CodeAction>();
|
|
67
|
+
|
|
68
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
69
|
+
this.language = language;
|
|
70
|
+
const { editor } = language;
|
|
71
|
+
this.refresher = new Refresher((signal) => this.hint(signal), PAUSE_MS, timers);
|
|
72
|
+
const providers = language.onProvidersChanged(() => {
|
|
73
|
+
this.refresher.later();
|
|
74
|
+
});
|
|
75
|
+
const offs = [
|
|
76
|
+
editor.onCaret(() => {
|
|
77
|
+
editor.setGutterAction(null);
|
|
78
|
+
this.refresher.later();
|
|
79
|
+
}),
|
|
80
|
+
editor.onTextChange(() => editor.setGutterAction(null)),
|
|
81
|
+
editor.onGutterAction(() => void this.open()),
|
|
82
|
+
editor.onKeyDown((event) => {
|
|
83
|
+
if (isShortcut(this.language.shortcuts.codeActions, event)) {
|
|
84
|
+
void this.open();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (isShortcut(this.language.shortcuts.refactorThis, event)) {
|
|
88
|
+
void this.refactorThis();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}),
|
|
93
|
+
language.diagnostics.onChange(() => this.refresher.later()),
|
|
94
|
+
providers
|
|
95
|
+
];
|
|
96
|
+
language.onDispose(() => {
|
|
97
|
+
for (const off of offs) {
|
|
98
|
+
off();
|
|
99
|
+
}
|
|
100
|
+
this.refresher.dispose();
|
|
101
|
+
editor.setGutterAction(null);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get supported(): boolean {
|
|
106
|
+
return this.language.project.service.supports(METHOD, this.language.uri);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* The list under the caret, or under the selection's start. */
|
|
110
|
+
open(): Promise<void> {
|
|
111
|
+
const range = this.language.editor.getSelection();
|
|
112
|
+
return this.ask({ range, anchor: range.start, withLine: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Refactor This: only the refactorings, grouped by what they do, under the caret or the selection. */
|
|
116
|
+
refactorThis(): Promise<void> {
|
|
117
|
+
const range = this.language.editor.getSelection();
|
|
118
|
+
return this.ask({ range, only: ['refactor'], anchor: range.start, groups: REFACTOR_GROUPS });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* The quick fixes of one problem, under it, as the Quick fix button of its card asks. */
|
|
122
|
+
quickFixFor(problem: Problem): Promise<void> {
|
|
123
|
+
const { range } = problem.diagnostic;
|
|
124
|
+
return this.ask({ range, only: ['quickfix'], anchor: range.start, diagnostics: [problem.diagnostic] });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async organizeImports(): Promise<void> {
|
|
128
|
+
const { editor } = this.language;
|
|
129
|
+
const everything = { start: { line: 0, character: 0 }, end: editor.positionAt(Number.MAX_SAFE_INTEGER) };
|
|
130
|
+
const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
|
|
131
|
+
const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
|
|
132
|
+
if (entry === undefined) {
|
|
133
|
+
this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await this.apply(entry);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async formatDocument(): Promise<void> {
|
|
140
|
+
const { editor, project, uri } = this.language;
|
|
141
|
+
if (!project.service.supports('textDocument/formatting', uri)) {
|
|
142
|
+
this.tell('error', this.say('noFormatter'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const edits = await project.service.formatting(uri, { ...editor.getIndentation() });
|
|
147
|
+
if (edits !== null && edits.length > 0 && !editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })))) {
|
|
148
|
+
this.tell('error', this.say('readOnly'));
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.fail(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
|
|
156
|
+
list(only: readonly string[]): Promise<ActionEntry[] | null> {
|
|
157
|
+
return this.supported
|
|
158
|
+
? this.request(
|
|
159
|
+
this.language.editor.getSelection(),
|
|
160
|
+
only,
|
|
161
|
+
AUTOMATIC,
|
|
162
|
+
undefined,
|
|
163
|
+
undefined,
|
|
164
|
+
only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS
|
|
165
|
+
)
|
|
166
|
+
: Promise.resolve(null);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async ask(options: AskOptions): Promise<void> {
|
|
170
|
+
if (!this.supported) {
|
|
171
|
+
this.tell('error', this.say('unavailable'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// The press on a card or the gutter may have taken the focus, and the list answers to the editor's keys.
|
|
175
|
+
this.language.editor.focus();
|
|
176
|
+
this.asking = true;
|
|
177
|
+
let entries: ActionEntry[] | null;
|
|
178
|
+
try {
|
|
179
|
+
entries = options.withLine
|
|
180
|
+
? await this.requestWithLine(options.range, INVOKED)
|
|
181
|
+
: await this.request(options.range, options.only, INVOKED, undefined, options.diagnostics, options.groups);
|
|
182
|
+
} finally {
|
|
183
|
+
this.asking = false;
|
|
184
|
+
}
|
|
185
|
+
if (entries === null) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (entries.length === 0) {
|
|
189
|
+
this.tell('success', this.say('none'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
193
|
+
const groups: PickGroup[] = (options.groups ?? ACTION_GROUPS).flatMap((group): PickGroup[] => {
|
|
194
|
+
const rows = entries.filter((entry) => entry.group === group).map((entry) => ({ id: entry.id, label: entry.action.title, detail: '' }));
|
|
195
|
+
return rows.length === 0 ? [] : [{ title: this.say(`groups.${group}`), rows }];
|
|
196
|
+
});
|
|
197
|
+
this.language.completion.close();
|
|
198
|
+
this.language.pick.open({
|
|
199
|
+
anchor: options.anchor,
|
|
200
|
+
groups,
|
|
201
|
+
accept: (id) => {
|
|
202
|
+
const entry = byId.get(id);
|
|
203
|
+
if (entry !== undefined) {
|
|
204
|
+
void this.apply(entry);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
attempt: (id) => this.refusalOf(byId.get(id)),
|
|
208
|
+
preview: (id, signal) => this.preview(byId.get(id), signal)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* 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> {
|
|
221
|
+
const { project, uri, diagnostics } = this.language;
|
|
222
|
+
try {
|
|
223
|
+
const context = {
|
|
224
|
+
diagnostics: [
|
|
225
|
+
...(about ??
|
|
226
|
+
diagnosticsAt(
|
|
227
|
+
diagnostics.problems.map((problem) => problem.diagnostic),
|
|
228
|
+
range
|
|
229
|
+
))
|
|
230
|
+
],
|
|
231
|
+
...(only ? { only: [...only] } : {}),
|
|
232
|
+
triggerKind: triggerKind as 1 | 2
|
|
233
|
+
};
|
|
234
|
+
const entries = actionsOf(await project.service.codeActions(uri, range, context, { signal }), order);
|
|
235
|
+
// A server may answer with more than the kinds asked for.
|
|
236
|
+
return only ? entries.filter((entry) => only.some((kind) => entry.action.kind === kind || entry.action.kind?.startsWith(`${kind}.`))) : entries;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (triggerKind === INVOKED) {
|
|
239
|
+
this.fail(error);
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/*
|
|
246
|
+
* What the servers offer at the range, and when no problem touches it, the quick fixes of the problems
|
|
247
|
+
* elsewhere on its line too, since the fix of an error is wanted from anywhere on the line it is on.
|
|
248
|
+
*/
|
|
249
|
+
private async requestWithLine(range: EditorRange, triggerKind: number, signal?: AbortSignal): Promise<ActionEntry[] | null> {
|
|
250
|
+
const entries = await this.request(range, undefined, triggerKind, signal);
|
|
251
|
+
if (entries === null || range.start.line !== range.end.line) {
|
|
252
|
+
return entries;
|
|
253
|
+
}
|
|
254
|
+
const all = this.language.diagnostics.problems.map((problem) => problem.diagnostic);
|
|
255
|
+
if (diagnosticsAt(all, range).length > 0) {
|
|
256
|
+
return entries;
|
|
257
|
+
}
|
|
258
|
+
const onLine = fixableOnLine(all, range.start.line).slice(0, LINE_PROBLEMS);
|
|
259
|
+
const fixes = await Promise.all(onLine.map((diagnostic) => this.request(diagnostic.range, ['quickfix'], triggerKind, signal, [diagnostic])));
|
|
260
|
+
return mergeEntries([entries, ...fixes.map((list) => list ?? [])]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async hint(signal: AbortSignal): Promise<void> {
|
|
264
|
+
const { editor } = this.language;
|
|
265
|
+
if (this.asking || !this.supported) {
|
|
266
|
+
editor.setGutterAction(null);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const range = editor.getSelection();
|
|
270
|
+
const entries = await this.requestWithLine(range, AUTOMATIC, signal);
|
|
271
|
+
if (signal.aborted || this.asking) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
editor.setGutterAction(entries?.some(isHint) === true ? { line: editor.getCaret().line, label: this.say('show') } : null);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async resolve(entry: ActionEntry, signal?: AbortSignal): Promise<CodeAction> {
|
|
278
|
+
const known = this.resolved.get(entry);
|
|
279
|
+
if (known !== undefined) {
|
|
280
|
+
return known;
|
|
281
|
+
}
|
|
282
|
+
const { project, uri } = this.language;
|
|
283
|
+
const { action } = entry;
|
|
284
|
+
let result = action;
|
|
285
|
+
if (action.edit === undefined && project.service.providerOptions(METHOD, uri)?.resolveProvider === true) {
|
|
286
|
+
try {
|
|
287
|
+
result = await project.service.resolveCodeAction(uri, action, { signal });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (isRefusal(error)) {
|
|
290
|
+
throw new ActionRefused((error as Error).message);
|
|
291
|
+
}
|
|
292
|
+
// An action the server cannot resolve is still tried as it is, unless the text moved under it.
|
|
293
|
+
if (error instanceof StaleResultError) {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
this.resolved.set(entry, result);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async preview(entry: ActionEntry | undefined, signal: AbortSignal): Promise<PickPreview | null> {
|
|
303
|
+
if (entry === undefined) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
let action: CodeAction;
|
|
307
|
+
try {
|
|
308
|
+
action = await this.resolve(entry, signal);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof ActionRefused) {
|
|
311
|
+
return { removed: [], added: [], note: null, refusal: error.message };
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
const { editor, uri } = this.language;
|
|
316
|
+
const found = action.edit === undefined ? null : previewOf(editor.getText(), action.edit, uri);
|
|
317
|
+
if (found === null) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const notes = [
|
|
321
|
+
...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
|
|
322
|
+
...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
|
|
323
|
+
...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
|
|
324
|
+
];
|
|
325
|
+
return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Why a server would not make the action, or null when it will, which resolves it for `apply` to use. */
|
|
329
|
+
private async refusalOf(entry: ActionEntry | undefined): Promise<string | null> {
|
|
330
|
+
if (entry === undefined) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await this.resolve(entry);
|
|
335
|
+
return null;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return error instanceof ActionRefused ? error.message : null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* Resolves the action, makes its edit as one undo step, and then runs its command. */
|
|
342
|
+
async apply(entry: ActionEntry): Promise<void> {
|
|
343
|
+
const { editor, project, uri } = this.language;
|
|
344
|
+
try {
|
|
345
|
+
const action = await this.resolve(entry);
|
|
346
|
+
if (action.edit !== undefined) {
|
|
347
|
+
const result = await project.applyWorkspaceEdit(action.edit);
|
|
348
|
+
if (!result.applied) {
|
|
349
|
+
this.tell('error', this.say('failed', { message: result.failureReason ?? this.say('refused') }));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (action.command?.command === INLINE_RENAME_COMMAND) {
|
|
354
|
+
await this.startRename(action.command);
|
|
355
|
+
} else if (action.command !== undefined) {
|
|
356
|
+
await project.service.executeCommand(uri, action.command);
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.fail(error);
|
|
360
|
+
} finally {
|
|
361
|
+
this.resolved.delete(entry);
|
|
362
|
+
editor.focus();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* The name a refactor wrote is offered for a rename right where it stands, the way the platform does after an extraction. */
|
|
367
|
+
private async startRename(command: Command): Promise<void> {
|
|
368
|
+
const target = (command.arguments?.[0] ?? null) as { textDocument?: { uri?: string }; position?: Position } | null;
|
|
369
|
+
if (target?.position !== undefined && target.textDocument?.uri === this.language.uri) {
|
|
370
|
+
this.language.editor.setCaret(target.position);
|
|
371
|
+
await this.language.rename.start();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private fail(error: unknown): void {
|
|
376
|
+
if (!(error instanceof StaleResultError)) {
|
|
377
|
+
this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private say(key: string, options?: Record<string, unknown>): string {
|
|
382
|
+
return this.language.project.i18n.t(`editor:language.actions.${key}`, options);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { i18n } from 'i18next';
|
|
2
|
+
import { diffLines, splitLines } from '@adecore/merge';
|
|
3
|
+
import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
|
|
4
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
5
|
+
|
|
6
|
+
/* What a line of the text in the editor is: the index of the commit that wrote it, or this for a line no commit holds yet. */
|
|
7
|
+
export const UNCOMMITTED = -1;
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The commit of every line of the text in the editor. Blame was run for the text on disk, so a line
|
|
11
|
+
* the editor still has from there keeps its commit and a line that was typed since, replaced or
|
|
12
|
+
* pasted has none, which reads as uncommitted. Null when the blame is not of that text.
|
|
13
|
+
*/
|
|
14
|
+
export function mapBlame(blame: GitBlameResult, base: string, current: readonly string[]): Int32Array | null {
|
|
15
|
+
const baseLines = splitLines(base);
|
|
16
|
+
if (blame.lines.length !== baseLines.length) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const mapped = new Int32Array(current.length).fill(UNCOMMITTED);
|
|
20
|
+
let basePosition = 0;
|
|
21
|
+
let position = 0;
|
|
22
|
+
const carry = (until: number): void => {
|
|
23
|
+
for (; position < until; position++, basePosition++) {
|
|
24
|
+
mapped[position] = blame.lines[basePosition]!;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
for (const change of diffLines(baseLines, current)) {
|
|
28
|
+
carry(change.otherStart);
|
|
29
|
+
position = change.otherEnd;
|
|
30
|
+
basePosition = change.baseEnd;
|
|
31
|
+
}
|
|
32
|
+
carry(current.length);
|
|
33
|
+
return mapped;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CodeAuthor {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly email: string;
|
|
39
|
+
/* The lines of the range this author wrote, blank ones left out. */
|
|
40
|
+
readonly lines: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CodeAuthorship {
|
|
44
|
+
/* Most lines first; one who wrote as many as another comes first by name. */
|
|
45
|
+
readonly authors: readonly CodeAuthor[];
|
|
46
|
+
/* Lines of the range no commit holds yet, blank ones left out. */
|
|
47
|
+
readonly uncommittedLines: number;
|
|
48
|
+
/* Whether any line of the range, a blank one too, is changed since the commit it came from. */
|
|
49
|
+
readonly modified: boolean;
|
|
50
|
+
/* The newest commit that wrote a line of the range. */
|
|
51
|
+
readonly latest: GitBlameCommit | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* A person's name as git has it, with runs of space made one. */
|
|
55
|
+
export function shortName(name: string): string {
|
|
56
|
+
return name.replace(/\s+/g, ' ').trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Who wrote the lines `from` through `to`. Blank lines count for no one and a line that is not
|
|
61
|
+
* committed for no author, so a method that is only edited keeps its author and says it was edited.
|
|
62
|
+
*/
|
|
63
|
+
export function authorshipOf(commits: readonly GitBlameCommit[], mapped: Int32Array, lines: readonly string[], from: number, to: number): CodeAuthorship {
|
|
64
|
+
const byName = new Map<string, { name: string; email: string; lines: number }>();
|
|
65
|
+
let uncommittedLines = 0;
|
|
66
|
+
let modified = false;
|
|
67
|
+
let latest: GitBlameCommit | null = null;
|
|
68
|
+
for (let line = from; line <= to && line < mapped.length; line++) {
|
|
69
|
+
const index = mapped[line]!;
|
|
70
|
+
if (index === UNCOMMITTED) {
|
|
71
|
+
modified = true;
|
|
72
|
+
}
|
|
73
|
+
if (lines[line]!.trim() === '') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (index === UNCOMMITTED) {
|
|
77
|
+
uncommittedLines++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const commit = commits[index]!;
|
|
81
|
+
const name = shortName(commit.author);
|
|
82
|
+
if (name === '') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const author = byName.get(name) ?? { name, email: commit.email, lines: 0 };
|
|
86
|
+
author.lines++;
|
|
87
|
+
byName.set(name, author);
|
|
88
|
+
if (latest === null || commit.at > latest.at) {
|
|
89
|
+
latest = commit;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const authors = [...byName.values()].sort((left, right) => right.lines - left.lines || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
93
|
+
return { authors, uncommittedLines, modified, latest };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* The row's words: the author with most lines, how many others wrote some, and a star when the range is edited since the commit. */
|
|
97
|
+
export function authorsText(authorship: CodeAuthorship, i18n: i18n): string {
|
|
98
|
+
const main = authorship.authors[0];
|
|
99
|
+
if (main === undefined) {
|
|
100
|
+
return i18n.t('editor:language.codeVision.newCode');
|
|
101
|
+
}
|
|
102
|
+
const others = authorship.authors.length - 1;
|
|
103
|
+
return `${main.name}${others > 0 ? ` +${formatNumber(others)}` : ''}${authorship.modified ? ' *' : ''}`;
|
|
104
|
+
}
|