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