aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/commands')
-rw-r--r--editors/code/src/commands/index.ts283
-rw-r--r--editors/code/src/commands/runnables.ts218
-rw-r--r--editors/code/src/commands/syntax_tree.ts263
3 files changed, 0 insertions, 764 deletions
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
deleted file mode 100644
index 1585912a2..000000000
--- a/editors/code/src/commands/index.ts
+++ /dev/null
@@ -1,283 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api';
4
5import { Ctx, Cmd } from '../ctx';
6import { applySnippetWorkspaceEdit } from '../snippets';
7import { spawnSync } from 'child_process';
8
9export * from './syntax_tree';
10export * from './runnables';
11
12export function analyzerStatus(ctx: Ctx): Cmd {
13 const tdcp = new class implements vscode.TextDocumentContentProvider {
14 readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
15 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
16
17 provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
18 if (!vscode.window.activeTextEditor) return '';
19
20 return ctx.client.sendRequest(ra.analyzerStatus, null);
21 }
22
23 get onDidChange(): vscode.Event<vscode.Uri> {
24 return this.eventEmitter.event;
25 }
26 }();
27
28 let poller: NodeJS.Timer | undefined = undefined;
29
30 ctx.pushCleanup(
31 vscode.workspace.registerTextDocumentContentProvider(
32 'rust-analyzer-status',
33 tdcp,
34 ),
35 );
36
37 ctx.pushCleanup({
38 dispose() {
39 if (poller !== undefined) {
40 clearInterval(poller);
41 }
42 },
43 });
44
45 return async () => {
46 if (poller === undefined) {
47 poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
48 }
49 const document = await vscode.workspace.openTextDocument(tdcp.uri);
50 return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
51 };
52}
53
54export function matchingBrace(ctx: Ctx): Cmd {
55 return async () => {
56 const editor = ctx.activeRustEditor;
57 const client = ctx.client;
58 if (!editor || !client) return;
59
60 const response = await client.sendRequest(ra.matchingBrace, {
61 textDocument: { uri: editor.document.uri.toString() },
62 positions: editor.selections.map(s =>
63 client.code2ProtocolConverter.asPosition(s.active),
64 ),
65 });
66 editor.selections = editor.selections.map((sel, idx) => {
67 const active = client.protocol2CodeConverter.asPosition(
68 response[idx],
69 );
70 const anchor = sel.isEmpty ? active : sel.anchor;
71 return new vscode.Selection(anchor, active);
72 });
73 editor.revealRange(editor.selection);
74 };
75}
76
77export function joinLines(ctx: Ctx): Cmd {
78 return async () => {
79 const editor = ctx.activeRustEditor;
80 const client = ctx.client;
81 if (!editor || !client) return;
82
83 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
84 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
85 textDocument: { uri: editor.document.uri.toString() },
86 });
87 editor.edit((builder) => {
88 client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
89 builder.replace(edit.range, edit.newText);
90 });
91 });
92 };
93}
94
95export function onEnter(ctx: Ctx): Cmd {
96 async function handleKeypress() {
97 const editor = ctx.activeRustEditor;
98 const client = ctx.client;
99
100 if (!editor || !client) return false;
101
102 const change = await client.sendRequest(ra.onEnter, {
103 textDocument: { uri: editor.document.uri.toString() },
104 position: client.code2ProtocolConverter.asPosition(
105 editor.selection.active,
106 ),
107 }).catch(_error => {
108 // client.logFailedRequest(OnEnterRequest.type, error);
109 return null;
110 });
111 if (!change) return false;
112
113 const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
114 await applySnippetWorkspaceEdit(workspaceEdit);
115 return true;
116 }
117
118 return async () => {
119 if (await handleKeypress()) return;
120
121 await vscode.commands.executeCommand('default:type', { text: '\n' });
122 };
123}
124
125export function parentModule(ctx: Ctx): Cmd {
126 return async () => {
127 const editor = ctx.activeRustEditor;
128 const client = ctx.client;
129 if (!editor || !client) return;
130
131 const response = await client.sendRequest(ra.parentModule, {
132 textDocument: { uri: editor.document.uri.toString() },
133 position: client.code2ProtocolConverter.asPosition(
134 editor.selection.active,
135 ),
136 });
137 const loc = response[0];
138 if (loc == null) return;
139
140 const uri = client.protocol2CodeConverter.asUri(loc.uri);
141 const range = client.protocol2CodeConverter.asRange(loc.range);
142
143 const doc = await vscode.workspace.openTextDocument(uri);
144 const e = await vscode.window.showTextDocument(doc);
145 e.selection = new vscode.Selection(range.start, range.start);
146 e.revealRange(range, vscode.TextEditorRevealType.InCenter);
147 };
148}
149
150export function ssr(ctx: Ctx): Cmd {
151 return async () => {
152 const client = ctx.client;
153 if (!client) return;
154
155 const options: vscode.InputBoxOptions = {
156 value: "() ==>> ()",
157 prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
158 validateInput: async (x: string) => {
159 try {
160 await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
161 } catch (e) {
162 return e.toString();
163 }
164 return null;
165 }
166 };
167 const request = await vscode.window.showInputBox(options);
168 if (!request) return;
169
170 const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
171
172 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
173 };
174}
175
176export function serverVersion(ctx: Ctx): Cmd {
177 return async () => {
178 const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
179 const commitHash = stdout.slice(`rust-analyzer `.length).trim();
180 const { releaseTag } = ctx.config.package;
181
182 void vscode.window.showInformationMessage(
183 `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
184 );
185 };
186}
187
188export function toggleInlayHints(ctx: Ctx): Cmd {
189 return async () => {
190 await vscode
191 .workspace
192 .getConfiguration(`${ctx.config.rootSection}.inlayHints`)
193 .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace);
194 };
195}
196
197// Opens the virtual file that will show the syntax tree
198//
199// The contents of the file come from the `TextDocumentContentProvider`
200export function expandMacro(ctx: Ctx): Cmd {
201 function codeFormat(expanded: ra.ExpandedMacro): string {
202 let result = `// Recursive expansion of ${expanded.name}! macro\n`;
203 result += '// ' + '='.repeat(result.length - 3);
204 result += '\n\n';
205 result += expanded.expansion;
206
207 return result;
208 }
209
210 const tdcp = new class implements vscode.TextDocumentContentProvider {
211 uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
212 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
213 async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
214 const editor = vscode.window.activeTextEditor;
215 const client = ctx.client;
216 if (!editor || !client) return '';
217
218 const position = editor.selection.active;
219
220 const expanded = await client.sendRequest(ra.expandMacro, {
221 textDocument: { uri: editor.document.uri.toString() },
222 position,
223 });
224
225 if (expanded == null) return 'Not available';
226
227 return codeFormat(expanded);
228 }
229
230 get onDidChange(): vscode.Event<vscode.Uri> {
231 return this.eventEmitter.event;
232 }
233 }();
234
235 ctx.pushCleanup(
236 vscode.workspace.registerTextDocumentContentProvider(
237 'rust-analyzer',
238 tdcp,
239 ),
240 );
241
242 return async () => {
243 const document = await vscode.workspace.openTextDocument(tdcp.uri);
244 tdcp.eventEmitter.fire(tdcp.uri);
245 return vscode.window.showTextDocument(
246 document,
247 vscode.ViewColumn.Two,
248 true,
249 );
250 };
251}
252
253export function collectGarbage(ctx: Ctx): Cmd {
254 return async () => ctx.client.sendRequest(ra.collectGarbage, null);
255}
256
257export function showReferences(ctx: Ctx): Cmd {
258 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
259 const client = ctx.client;
260 if (client) {
261 vscode.commands.executeCommand(
262 'editor.action.showReferences',
263 vscode.Uri.parse(uri),
264 client.protocol2CodeConverter.asPosition(position),
265 locations.map(client.protocol2CodeConverter.asLocation),
266 );
267 }
268 };
269}
270
271export function applyActionGroup(_ctx: Ctx): Cmd {
272 return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
273 const selectedAction = await vscode.window.showQuickPick(actions);
274 if (!selectedAction) return;
275 await applySnippetWorkspaceEdit(selectedAction.edit);
276 };
277}
278
279export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
280 return async (edit: vscode.WorkspaceEdit) => {
281 await applySnippetWorkspaceEdit(edit);
282 };
283}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
deleted file mode 100644
index 0bd30fb07..000000000
--- a/editors/code/src/commands/runnables.ts
+++ /dev/null
@@ -1,218 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api';
4
5import { Ctx, Cmd } from '../ctx';
6import { startDebugSession, getDebugConfiguration } from '../debug';
7
8const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
9
10async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
11 const editor = ctx.activeRustEditor;
12 const client = ctx.client;
13 if (!editor || !client) return;
14
15 const textDocument: lc.TextDocumentIdentifier = {
16 uri: editor.document.uri.toString(),
17 };
18
19 const runnables = await client.sendRequest(ra.runnables, {
20 textDocument,
21 position: client.code2ProtocolConverter.asPosition(
22 editor.selection.active,
23 ),
24 });
25 const items: RunnableQuickPick[] = [];
26 if (prevRunnable) {
27 items.push(prevRunnable);
28 }
29 for (const r of runnables) {
30 if (
31 prevRunnable &&
32 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
33 ) {
34 continue;
35 }
36
37 if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) {
38 continue;
39 }
40 items.push(new RunnableQuickPick(r));
41 }
42
43 if (items.length === 0) {
44 // it is the debug case, run always has at least 'cargo check ...'
45 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables
46 vscode.window.showErrorMessage("There's no debug target!");
47 return;
48 }
49
50 return await new Promise((resolve) => {
51 const disposables: vscode.Disposable[] = [];
52 const close = (result?: RunnableQuickPick) => {
53 resolve(result);
54 disposables.forEach(d => d.dispose());
55 };
56
57 const quickPick = vscode.window.createQuickPick<RunnableQuickPick>();
58 quickPick.items = items;
59 quickPick.title = "Select Runnable";
60 if (showButtons) {
61 quickPick.buttons = quickPickButtons;
62 }
63 disposables.push(
64 quickPick.onDidHide(() => close()),
65 quickPick.onDidAccept(() => close(quickPick.selectedItems[0])),
66 quickPick.onDidTriggerButton((_button) => {
67 (async () => await makeDebugConfig(ctx, quickPick.activeItems[0]))();
68 close();
69 }),
70 quickPick.onDidChangeActive((active) => {
71 if (showButtons && active.length > 0) {
72 if (active[0].label.startsWith('cargo')) {
73 // save button makes no sense for `cargo test` or `cargo check`
74 quickPick.buttons = [];
75 } else if (quickPick.buttons.length === 0) {
76 quickPick.buttons = quickPickButtons;
77 }
78 }
79 }),
80 quickPick
81 );
82 quickPick.show();
83 });
84}
85
86export function run(ctx: Ctx): Cmd {
87 let prevRunnable: RunnableQuickPick | undefined;
88
89 return async () => {
90 const item = await selectRunnable(ctx, prevRunnable);
91 if (!item) return;
92
93 item.detail = 'rerun';
94 prevRunnable = item;
95 const task = createTask(item.runnable);
96 return await vscode.tasks.executeTask(task);
97 };
98}
99
100export function runSingle(ctx: Ctx): Cmd {
101 return async (runnable: ra.Runnable) => {
102 const editor = ctx.activeRustEditor;
103 if (!editor) return;
104
105 const task = createTask(runnable);
106 task.group = vscode.TaskGroup.Build;
107 task.presentationOptions = {
108 reveal: vscode.TaskRevealKind.Always,
109 panel: vscode.TaskPanelKind.Dedicated,
110 clear: true,
111 };
112
113 return vscode.tasks.executeTask(task);
114 };
115}
116
117export function debug(ctx: Ctx): Cmd {
118 let prevDebuggee: RunnableQuickPick | undefined;
119
120 return async () => {
121 const item = await selectRunnable(ctx, prevDebuggee, true);
122 if (!item) return;
123
124 item.detail = 'restart';
125 prevDebuggee = item;
126 return await startDebugSession(ctx, item.runnable);
127 };
128}
129
130export function debugSingle(ctx: Ctx): Cmd {
131 return async (config: ra.Runnable) => {
132 await startDebugSession(ctx, config);
133 };
134}
135
136async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise<void> {
137 const scope = ctx.activeRustEditor?.document.uri;
138 if (!scope) return;
139
140 const debugConfig = await getDebugConfiguration(ctx, item.runnable);
141 if (!debugConfig) return;
142
143 const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
144 const configurations = wsLaunchSection.get<any[]>("configurations") || [];
145
146 const index = configurations.findIndex(c => c.name === debugConfig.name);
147 if (index !== -1) {
148 const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update');
149 if (answer === "Cancel") return;
150
151 configurations[index] = debugConfig;
152 } else {
153 configurations.push(debugConfig);
154 }
155
156 await wsLaunchSection.update("configurations", configurations);
157}
158
159export function newDebugConfig(ctx: Ctx): Cmd {
160 return async () => {
161 const item = await selectRunnable(ctx, undefined, true, false);
162 if (!item) return;
163
164 await makeDebugConfig(ctx, item);
165 };
166}
167
168class RunnableQuickPick implements vscode.QuickPickItem {
169 public label: string;
170 public description?: string | undefined;
171 public detail?: string | undefined;
172 public picked?: boolean | undefined;
173
174 constructor(public runnable: ra.Runnable) {
175 this.label = runnable.label;
176 }
177}
178
179interface CargoTaskDefinition extends vscode.TaskDefinition {
180 type: 'cargo';
181 label: string;
182 command: string;
183 args: string[];
184 env?: { [key: string]: string };
185}
186
187function createTask(spec: ra.Runnable): vscode.Task {
188 const TASK_SOURCE = 'Rust';
189 const definition: CargoTaskDefinition = {
190 type: 'cargo',
191 label: spec.label,
192 command: spec.bin,
193 args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
194 env: spec.env,
195 };
196
197 const execOption: vscode.ShellExecutionOptions = {
198 cwd: spec.cwd || '.',
199 env: definition.env,
200 };
201 const exec = new vscode.ShellExecution(
202 definition.command,
203 definition.args,
204 execOption,
205 );
206
207 const f = vscode.workspace.workspaceFolders![0];
208 const t = new vscode.Task(
209 definition,
210 f,
211 definition.label,
212 TASK_SOURCE,
213 exec,
214 ['$rustc'],
215 );
216 t.presentationOptions.clear = true;
217 return t;
218}
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
deleted file mode 100644
index a5446c327..000000000
--- a/editors/code/src/commands/syntax_tree.ts
+++ /dev/null
@@ -1,263 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd, Disposable } from '../ctx';
5import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7const AST_FILE_SCHEME = "rust-analyzer";
8
9// Opens the virtual file that will show the syntax tree
10//
11// The contents of the file come from the `TextDocumentContentProvider`
12export function syntaxTree(ctx: Ctx): Cmd {
13 const tdcp = new TextDocumentContentProvider(ctx);
14
15 void new AstInspector(ctx);
16
17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
18 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
19 brackets: [["[", ")"]],
20 }));
21
22 return async () => {
23 const editor = vscode.window.activeTextEditor;
24 const rangeEnabled = !!editor && !editor.selection.isEmpty;
25
26 const uri = rangeEnabled
27 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
28 : tdcp.uri;
29
30 const document = await vscode.workspace.openTextDocument(uri);
31
32 tdcp.eventEmitter.fire(uri);
33
34 void await vscode.window.showTextDocument(document, {
35 viewColumn: vscode.ViewColumn.Two,
36 preserveFocus: true
37 });
38 };
39}
40
41class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
42 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
43 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
44
45
46 constructor(private readonly ctx: Ctx) {
47 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
48 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
49 }
50
51 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
52 if (isRustDocument(event.document)) {
53 // We need to order this after language server updates, but there's no API for that.
54 // Hence, good old sleep().
55 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
56 }
57 }
58 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
59 if (editor && isRustEditor(editor)) {
60 this.eventEmitter.fire(this.uri);
61 }
62 }
63
64 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
65 const rustEditor = this.ctx.activeRustEditor;
66 if (!rustEditor) return '';
67
68 // When the range based query is enabled we take the range of the selection
69 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
70 ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
71 : null;
72
73 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
74 return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
75 }
76
77 get onDidChange(): vscode.Event<vscode.Uri> {
78 return this.eventEmitter.event;
79 }
80}
81
82
83// FIXME: consider implementing this via the Tree View API?
84// https://code.visualstudio.com/api/extension-guides/tree-view
85class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
86 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
88 borderStyle: "solid",
89 borderWidth: "2px",
90
91 });
92 private rustEditor: undefined | RustEditor;
93
94 // Lazy rust token range -> syntax tree file range.
95 private readonly rust2Ast = new Lazy(() => {
96 const astEditor = this.findAstTextEditor();
97 if (!this.rustEditor || !astEditor) return undefined;
98
99 const buf: [vscode.Range, vscode.Range][] = [];
100 for (let i = 0; i < astEditor.document.lineCount; ++i) {
101 const astLine = astEditor.document.lineAt(i);
102
103 // Heuristically look for nodes with quoted text (which are token nodes)
104 const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
105 if (!isTokenNode) continue;
106
107 const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
108 if (!rustRange) continue;
109
110 buf.push([rustRange, this.findAstNodeRange(astLine)]);
111 }
112 return buf;
113 });
114
115 constructor(ctx: Ctx) {
116 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
117 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
118 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
119 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
120 vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
121
122 ctx.pushCleanup(this);
123 }
124 dispose() {
125 this.setRustEditor(undefined);
126 }
127
128 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
129 if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
130 this.rust2Ast.reset();
131 }
132 }
133
134 private onDidCloseTextDocument(doc: vscode.TextDocument) {
135 if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
136 this.setRustEditor(undefined);
137 }
138 }
139
140 private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
141 if (!this.findAstTextEditor()) {
142 this.setRustEditor(undefined);
143 return;
144 }
145 this.setRustEditor(editors.find(isRustEditor));
146 }
147
148 private findAstTextEditor(): undefined | vscode.TextEditor {
149 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME);
150 }
151
152 private setRustEditor(newRustEditor: undefined | RustEditor) {
153 if (this.rustEditor && this.rustEditor !== newRustEditor) {
154 this.rustEditor.setDecorations(this.astDecorationType, []);
155 this.rust2Ast.reset();
156 }
157 this.rustEditor = newRustEditor;
158 }
159
160 // additional positional params are omitted
161 provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
162 if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
163
164 const astEditor = this.findAstTextEditor();
165 if (!astEditor) return;
166
167 const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
168 if (!rust2AstRanges) return;
169
170 const [rustFileRange, astFileRange] = rust2AstRanges;
171
172 astEditor.revealRange(astFileRange);
173 astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
174
175 return [{
176 targetRange: astFileRange,
177 targetUri: astEditor.document.uri,
178 originSelectionRange: rustFileRange,
179 targetSelectionRange: astFileRange,
180 }];
181 }
182
183 // additional positional params are omitted
184 provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
185 if (!this.rustEditor) return;
186
187 const astFileLine = doc.lineAt(hoverPosition.line);
188
189 const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
190 if (!rustFileRange) return;
191
192 this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
193 this.rustEditor.revealRange(rustFileRange);
194
195 const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
196 const astFileRange = this.findAstNodeRange(astFileLine);
197
198 return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
199 }
200
201 private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
202 const lineOffset = astLine.range.start;
203 const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
204 const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
205 return new vscode.Range(begin, end);
206 }
207
208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
209 const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
210 if (!parsedRange) return;
211
212 const [begin, end] = parsedRange
213 .slice(1)
214 .map(off => this.positionAt(doc, +off));
215
216 return new vscode.Range(begin, end);
217 }
218
219 // Memoize the last value, otherwise the CPU is at 100% single core
220 // with quadratic lookups when we build rust2Ast cache
221 cache?: { doc: vscode.TextDocument; offset: number; line: number };
222
223 positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
224 if (doc.eol === vscode.EndOfLine.LF) {
225 return doc.positionAt(targetOffset);
226 }
227
228 // Dirty workaround for crlf line endings
229 // We are still in this prehistoric era of carriage returns here...
230
231 let line = 0;
232 let offset = 0;
233
234 const cache = this.cache;
235 if (cache?.doc === doc && cache.offset <= targetOffset) {
236 ({ line, offset } = cache);
237 }
238
239 while (true) {
240 const lineLenWithLf = doc.lineAt(line).text.length + 1;
241 if (offset + lineLenWithLf > targetOffset) {
242 this.cache = { doc, offset, line };
243 return doc.positionAt(targetOffset + line);
244 }
245 offset += lineLenWithLf;
246 line += 1;
247 }
248 }
249}
250
251class Lazy<T> {
252 val: undefined | T;
253
254 constructor(private readonly compute: () => undefined | T) { }
255
256 get() {
257 return this.val ?? (this.val = this.compute());
258 }
259
260 reset() {
261 this.val = undefined;
262 }
263}