diff options
Diffstat (limited to 'editors/code/src/commands.ts')
-rw-r--r-- | editors/code/src/commands.ts | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts new file mode 100644 index 000000000..534d2a984 --- /dev/null +++ b/editors/code/src/commands.ts | |||
@@ -0,0 +1,416 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import * as lc from 'vscode-languageclient'; | ||
3 | import * as ra from './lsp_ext'; | ||
4 | |||
5 | import { Ctx, Cmd } from './ctx'; | ||
6 | import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets'; | ||
7 | import { spawnSync } from 'child_process'; | ||
8 | import { RunnableQuickPick, selectRunnable, createTask } from './run'; | ||
9 | import { AstInspector } from './ast_inspector'; | ||
10 | import { isRustDocument, sleep, isRustEditor } from './util'; | ||
11 | import { startDebugSession, makeDebugConfig } from './debug'; | ||
12 | |||
13 | export * from './ast_inspector'; | ||
14 | export * from './run'; | ||
15 | |||
16 | export function analyzerStatus(ctx: Ctx): Cmd { | ||
17 | const tdcp = new class implements vscode.TextDocumentContentProvider { | ||
18 | readonly uri = vscode.Uri.parse('rust-analyzer-status://status'); | ||
19 | readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
20 | |||
21 | provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> { | ||
22 | if (!vscode.window.activeTextEditor) return ''; | ||
23 | |||
24 | return ctx.client.sendRequest(ra.analyzerStatus, null); | ||
25 | } | ||
26 | |||
27 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
28 | return this.eventEmitter.event; | ||
29 | } | ||
30 | }(); | ||
31 | |||
32 | let poller: NodeJS.Timer | undefined = undefined; | ||
33 | |||
34 | ctx.pushCleanup( | ||
35 | vscode.workspace.registerTextDocumentContentProvider( | ||
36 | 'rust-analyzer-status', | ||
37 | tdcp, | ||
38 | ), | ||
39 | ); | ||
40 | |||
41 | ctx.pushCleanup({ | ||
42 | dispose() { | ||
43 | if (poller !== undefined) { | ||
44 | clearInterval(poller); | ||
45 | } | ||
46 | }, | ||
47 | }); | ||
48 | |||
49 | return async () => { | ||
50 | if (poller === undefined) { | ||
51 | poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000); | ||
52 | } | ||
53 | const document = await vscode.workspace.openTextDocument(tdcp.uri); | ||
54 | return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true); | ||
55 | }; | ||
56 | } | ||
57 | |||
58 | export function matchingBrace(ctx: Ctx): Cmd { | ||
59 | return async () => { | ||
60 | const editor = ctx.activeRustEditor; | ||
61 | const client = ctx.client; | ||
62 | if (!editor || !client) return; | ||
63 | |||
64 | const response = await client.sendRequest(ra.matchingBrace, { | ||
65 | textDocument: { uri: editor.document.uri.toString() }, | ||
66 | positions: editor.selections.map(s => | ||
67 | client.code2ProtocolConverter.asPosition(s.active), | ||
68 | ), | ||
69 | }); | ||
70 | editor.selections = editor.selections.map((sel, idx) => { | ||
71 | const active = client.protocol2CodeConverter.asPosition( | ||
72 | response[idx], | ||
73 | ); | ||
74 | const anchor = sel.isEmpty ? active : sel.anchor; | ||
75 | return new vscode.Selection(anchor, active); | ||
76 | }); | ||
77 | editor.revealRange(editor.selection); | ||
78 | }; | ||
79 | } | ||
80 | |||
81 | export function joinLines(ctx: Ctx): Cmd { | ||
82 | return async () => { | ||
83 | const editor = ctx.activeRustEditor; | ||
84 | const client = ctx.client; | ||
85 | if (!editor || !client) return; | ||
86 | |||
87 | const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, { | ||
88 | ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)), | ||
89 | textDocument: { uri: editor.document.uri.toString() }, | ||
90 | }); | ||
91 | editor.edit((builder) => { | ||
92 | client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => { | ||
93 | builder.replace(edit.range, edit.newText); | ||
94 | }); | ||
95 | }); | ||
96 | }; | ||
97 | } | ||
98 | |||
99 | export function onEnter(ctx: Ctx): Cmd { | ||
100 | async function handleKeypress() { | ||
101 | const editor = ctx.activeRustEditor; | ||
102 | const client = ctx.client; | ||
103 | |||
104 | if (!editor || !client) return false; | ||
105 | |||
106 | const lcEdits = await client.sendRequest(ra.onEnter, { | ||
107 | textDocument: { uri: editor.document.uri.toString() }, | ||
108 | position: client.code2ProtocolConverter.asPosition( | ||
109 | editor.selection.active, | ||
110 | ), | ||
111 | }).catch(_error => { | ||
112 | // client.logFailedRequest(OnEnterRequest.type, error); | ||
113 | return null; | ||
114 | }); | ||
115 | if (!lcEdits) return false; | ||
116 | |||
117 | const edits = client.protocol2CodeConverter.asTextEdits(lcEdits); | ||
118 | await applySnippetTextEdits(editor, edits); | ||
119 | return true; | ||
120 | } | ||
121 | |||
122 | return async () => { | ||
123 | if (await handleKeypress()) return; | ||
124 | |||
125 | await vscode.commands.executeCommand('default:type', { text: '\n' }); | ||
126 | }; | ||
127 | } | ||
128 | |||
129 | export function parentModule(ctx: Ctx): Cmd { | ||
130 | return async () => { | ||
131 | const editor = ctx.activeRustEditor; | ||
132 | const client = ctx.client; | ||
133 | if (!editor || !client) return; | ||
134 | |||
135 | const response = await client.sendRequest(ra.parentModule, { | ||
136 | textDocument: { uri: editor.document.uri.toString() }, | ||
137 | position: client.code2ProtocolConverter.asPosition( | ||
138 | editor.selection.active, | ||
139 | ), | ||
140 | }); | ||
141 | const loc = response[0]; | ||
142 | if (!loc) return; | ||
143 | |||
144 | const uri = client.protocol2CodeConverter.asUri(loc.targetUri); | ||
145 | const range = client.protocol2CodeConverter.asRange(loc.targetRange); | ||
146 | |||
147 | const doc = await vscode.workspace.openTextDocument(uri); | ||
148 | const e = await vscode.window.showTextDocument(doc); | ||
149 | e.selection = new vscode.Selection(range.start, range.start); | ||
150 | e.revealRange(range, vscode.TextEditorRevealType.InCenter); | ||
151 | }; | ||
152 | } | ||
153 | |||
154 | export function ssr(ctx: Ctx): Cmd { | ||
155 | return async () => { | ||
156 | const client = ctx.client; | ||
157 | if (!client) return; | ||
158 | |||
159 | const options: vscode.InputBoxOptions = { | ||
160 | value: "() ==>> ()", | ||
161 | prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ", | ||
162 | validateInput: async (x: string) => { | ||
163 | try { | ||
164 | await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); | ||
165 | } catch (e) { | ||
166 | return e.toString(); | ||
167 | } | ||
168 | return null; | ||
169 | } | ||
170 | }; | ||
171 | const request = await vscode.window.showInputBox(options); | ||
172 | if (!request) return; | ||
173 | |||
174 | const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false }); | ||
175 | |||
176 | await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit)); | ||
177 | }; | ||
178 | } | ||
179 | |||
180 | export function serverVersion(ctx: Ctx): Cmd { | ||
181 | return async () => { | ||
182 | const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }); | ||
183 | const commitHash = stdout.slice(`rust-analyzer `.length).trim(); | ||
184 | const { releaseTag } = ctx.config.package; | ||
185 | |||
186 | void vscode.window.showInformationMessage( | ||
187 | `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})` | ||
188 | ); | ||
189 | }; | ||
190 | } | ||
191 | |||
192 | export function toggleInlayHints(ctx: Ctx): Cmd { | ||
193 | return async () => { | ||
194 | await vscode | ||
195 | .workspace | ||
196 | .getConfiguration(`${ctx.config.rootSection}.inlayHints`) | ||
197 | .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace); | ||
198 | }; | ||
199 | } | ||
200 | |||
201 | // Opens the virtual file that will show the syntax tree | ||
202 | // | ||
203 | // The contents of the file come from the `TextDocumentContentProvider` | ||
204 | export function syntaxTree(ctx: Ctx): Cmd { | ||
205 | const tdcp = new class implements vscode.TextDocumentContentProvider { | ||
206 | readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast'); | ||
207 | readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
208 | constructor() { | ||
209 | vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); | ||
210 | vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions); | ||
211 | } | ||
212 | |||
213 | private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { | ||
214 | if (isRustDocument(event.document)) { | ||
215 | // We need to order this after language server updates, but there's no API for that. | ||
216 | // Hence, good old sleep(). | ||
217 | void sleep(10).then(() => this.eventEmitter.fire(this.uri)); | ||
218 | } | ||
219 | } | ||
220 | private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { | ||
221 | if (editor && isRustEditor(editor)) { | ||
222 | this.eventEmitter.fire(this.uri); | ||
223 | } | ||
224 | } | ||
225 | |||
226 | provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> { | ||
227 | const rustEditor = ctx.activeRustEditor; | ||
228 | if (!rustEditor) return ''; | ||
229 | |||
230 | // When the range based query is enabled we take the range of the selection | ||
231 | const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty | ||
232 | ? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection) | ||
233 | : null; | ||
234 | |||
235 | const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, }; | ||
236 | return ctx.client.sendRequest(ra.syntaxTree, params, ct); | ||
237 | } | ||
238 | |||
239 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
240 | return this.eventEmitter.event; | ||
241 | } | ||
242 | }; | ||
243 | |||
244 | void new AstInspector(ctx); | ||
245 | |||
246 | ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider('rust-analyzer', tdcp)); | ||
247 | ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", { | ||
248 | brackets: [["[", ")"]], | ||
249 | })); | ||
250 | |||
251 | return async () => { | ||
252 | const editor = vscode.window.activeTextEditor; | ||
253 | const rangeEnabled = !!editor && !editor.selection.isEmpty; | ||
254 | |||
255 | const uri = rangeEnabled | ||
256 | ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) | ||
257 | : tdcp.uri; | ||
258 | |||
259 | const document = await vscode.workspace.openTextDocument(uri); | ||
260 | |||
261 | tdcp.eventEmitter.fire(uri); | ||
262 | |||
263 | void await vscode.window.showTextDocument(document, { | ||
264 | viewColumn: vscode.ViewColumn.Two, | ||
265 | preserveFocus: true | ||
266 | }); | ||
267 | }; | ||
268 | } | ||
269 | |||
270 | |||
271 | // Opens the virtual file that will show the syntax tree | ||
272 | // | ||
273 | // The contents of the file come from the `TextDocumentContentProvider` | ||
274 | export function expandMacro(ctx: Ctx): Cmd { | ||
275 | function codeFormat(expanded: ra.ExpandedMacro): string { | ||
276 | let result = `// Recursive expansion of ${expanded.name}! macro\n`; | ||
277 | result += '// ' + '='.repeat(result.length - 3); | ||
278 | result += '\n\n'; | ||
279 | result += expanded.expansion; | ||
280 | |||
281 | return result; | ||
282 | } | ||
283 | |||
284 | const tdcp = new class implements vscode.TextDocumentContentProvider { | ||
285 | uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs'); | ||
286 | eventEmitter = new vscode.EventEmitter<vscode.Uri>(); | ||
287 | async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> { | ||
288 | const editor = vscode.window.activeTextEditor; | ||
289 | const client = ctx.client; | ||
290 | if (!editor || !client) return ''; | ||
291 | |||
292 | const position = editor.selection.active; | ||
293 | |||
294 | const expanded = await client.sendRequest(ra.expandMacro, { | ||
295 | textDocument: { uri: editor.document.uri.toString() }, | ||
296 | position, | ||
297 | }); | ||
298 | |||
299 | if (expanded == null) return 'Not available'; | ||
300 | |||
301 | return codeFormat(expanded); | ||
302 | } | ||
303 | |||
304 | get onDidChange(): vscode.Event<vscode.Uri> { | ||
305 | return this.eventEmitter.event; | ||
306 | } | ||
307 | }(); | ||
308 | |||
309 | ctx.pushCleanup( | ||
310 | vscode.workspace.registerTextDocumentContentProvider( | ||
311 | 'rust-analyzer', | ||
312 | tdcp, | ||
313 | ), | ||
314 | ); | ||
315 | |||
316 | return async () => { | ||
317 | const document = await vscode.workspace.openTextDocument(tdcp.uri); | ||
318 | tdcp.eventEmitter.fire(tdcp.uri); | ||
319 | return vscode.window.showTextDocument( | ||
320 | document, | ||
321 | vscode.ViewColumn.Two, | ||
322 | true, | ||
323 | ); | ||
324 | }; | ||
325 | } | ||
326 | |||
327 | export function collectGarbage(ctx: Ctx): Cmd { | ||
328 | return async () => ctx.client.sendRequest(ra.collectGarbage, null); | ||
329 | } | ||
330 | |||
331 | export function showReferences(ctx: Ctx): Cmd { | ||
332 | return (uri: string, position: lc.Position, locations: lc.Location[]) => { | ||
333 | const client = ctx.client; | ||
334 | if (client) { | ||
335 | vscode.commands.executeCommand( | ||
336 | 'editor.action.showReferences', | ||
337 | vscode.Uri.parse(uri), | ||
338 | client.protocol2CodeConverter.asPosition(position), | ||
339 | locations.map(client.protocol2CodeConverter.asLocation), | ||
340 | ); | ||
341 | } | ||
342 | }; | ||
343 | } | ||
344 | |||
345 | export function applyActionGroup(_ctx: Ctx): Cmd { | ||
346 | return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => { | ||
347 | const selectedAction = await vscode.window.showQuickPick(actions); | ||
348 | if (!selectedAction) return; | ||
349 | await applySnippetWorkspaceEdit(selectedAction.edit); | ||
350 | }; | ||
351 | } | ||
352 | |||
353 | export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd { | ||
354 | return async (edit: vscode.WorkspaceEdit) => { | ||
355 | await applySnippetWorkspaceEdit(edit); | ||
356 | }; | ||
357 | } | ||
358 | |||
359 | export function run(ctx: Ctx): Cmd { | ||
360 | let prevRunnable: RunnableQuickPick | undefined; | ||
361 | |||
362 | return async () => { | ||
363 | const item = await selectRunnable(ctx, prevRunnable); | ||
364 | if (!item) return; | ||
365 | |||
366 | item.detail = 'rerun'; | ||
367 | prevRunnable = item; | ||
368 | const task = createTask(item.runnable); | ||
369 | return await vscode.tasks.executeTask(task); | ||
370 | }; | ||
371 | } | ||
372 | |||
373 | export function runSingle(ctx: Ctx): Cmd { | ||
374 | return async (runnable: ra.Runnable) => { | ||
375 | const editor = ctx.activeRustEditor; | ||
376 | if (!editor) return; | ||
377 | |||
378 | const task = createTask(runnable); | ||
379 | task.group = vscode.TaskGroup.Build; | ||
380 | task.presentationOptions = { | ||
381 | reveal: vscode.TaskRevealKind.Always, | ||
382 | panel: vscode.TaskPanelKind.Dedicated, | ||
383 | clear: true, | ||
384 | }; | ||
385 | |||
386 | return vscode.tasks.executeTask(task); | ||
387 | }; | ||
388 | } | ||
389 | |||
390 | export function debug(ctx: Ctx): Cmd { | ||
391 | let prevDebuggee: RunnableQuickPick | undefined; | ||
392 | |||
393 | return async () => { | ||
394 | const item = await selectRunnable(ctx, prevDebuggee, true); | ||
395 | if (!item) return; | ||
396 | |||
397 | item.detail = 'restart'; | ||
398 | prevDebuggee = item; | ||
399 | return await startDebugSession(ctx, item.runnable); | ||
400 | }; | ||
401 | } | ||
402 | |||
403 | export function debugSingle(ctx: Ctx): Cmd { | ||
404 | return async (config: ra.Runnable) => { | ||
405 | await startDebugSession(ctx, config); | ||
406 | }; | ||
407 | } | ||
408 | |||
409 | export function newDebugConfig(ctx: Ctx): Cmd { | ||
410 | return async () => { | ||
411 | const item = await selectRunnable(ctx, undefined, true, false); | ||
412 | if (!item) return; | ||
413 | |||
414 | await makeDebugConfig(ctx, item.runnable); | ||
415 | }; | ||
416 | } | ||