diff options
author | Mikhail Rakhmanov <[email protected]> | 2020-06-13 07:42:15 +0100 |
---|---|---|
committer | Mikhail Rakhmanov <[email protected]> | 2020-06-13 07:42:15 +0100 |
commit | 16bbf4ab7f132e6e5e5318dccdef9a5d71afdd7f (patch) | |
tree | 4b79fa8c046be56b02427ba843e70cdf3ac05767 /editors/code | |
parent | eeb8b9e236796da8734ba81a49164864497f7226 (diff) | |
parent | b56ad148db0c69eb279c225f45d324b4e80e7367 (diff) |
Merge branch 'master' into keyword_completion
# Conflicts:
# docs/user/generated_features.adoc
Diffstat (limited to 'editors/code')
-rw-r--r-- | editors/code/package.json | 64 | ||||
-rw-r--r-- | editors/code/src/client.ts | 106 | ||||
-rw-r--r-- | editors/code/src/commands.ts | 93 | ||||
-rw-r--r-- | editors/code/src/config.ts | 13 | ||||
-rw-r--r-- | editors/code/src/debug.ts | 121 | ||||
-rw-r--r-- | editors/code/src/lsp_ext.ts | 34 | ||||
-rw-r--r-- | editors/code/src/main.ts | 1 | ||||
-rw-r--r-- | editors/code/src/run.ts | 96 | ||||
-rw-r--r-- | editors/code/src/tasks.ts | 7 | ||||
-rw-r--r-- | editors/code/src/toolchain.ts (renamed from editors/code/src/cargo.ts) | 117 | ||||
-rw-r--r-- | editors/code/src/util.ts | 18 | ||||
-rw-r--r-- | editors/code/tests/unit/launch_config.test.ts | 14 |
12 files changed, 432 insertions, 252 deletions
diff --git a/editors/code/package.json b/editors/code/package.json index 75dbafc05..e2027970d 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -238,7 +238,7 @@ | |||
238 | }, | 238 | }, |
239 | "rust-analyzer.cargo.allFeatures": { | 239 | "rust-analyzer.cargo.allFeatures": { |
240 | "type": "boolean", | 240 | "type": "boolean", |
241 | "default": true, | 241 | "default": false, |
242 | "description": "Activate all available features" | 242 | "description": "Activate all available features" |
243 | }, | 243 | }, |
244 | "rust-analyzer.cargo.features": { | 244 | "rust-analyzer.cargo.features": { |
@@ -318,9 +318,23 @@ | |||
318 | "markdownDescription": "Check all targets and tests (will be passed as `--all-targets`)" | 318 | "markdownDescription": "Check all targets and tests (will be passed as `--all-targets`)" |
319 | }, | 319 | }, |
320 | "rust-analyzer.checkOnSave.allFeatures": { | 320 | "rust-analyzer.checkOnSave.allFeatures": { |
321 | "type": "boolean", | 321 | "type": [ |
322 | "default": true, | 322 | "null", |
323 | "markdownDescription": "Check with all features (will be passed as `--all-features`)" | 323 | "boolean" |
324 | ], | ||
325 | "default": null, | ||
326 | "markdownDescription": "Check with all features (will be passed as `--all-features`). Defaults to `rust-analyzer.cargo.allFeatures`." | ||
327 | }, | ||
328 | "rust-analyzer.checkOnSave.features": { | ||
329 | "type": [ | ||
330 | "null", | ||
331 | "array" | ||
332 | ], | ||
333 | "items": { | ||
334 | "type": "string" | ||
335 | }, | ||
336 | "default": null, | ||
337 | "description": "List of features to activate. Defaults to `rust-analyzer.cargo.features`." | ||
324 | }, | 338 | }, |
325 | "rust-analyzer.inlayHints.enable": { | 339 | "rust-analyzer.inlayHints.enable": { |
326 | "type": "boolean", | 340 | "type": "boolean", |
@@ -462,17 +476,53 @@ | |||
462 | "default": true | 476 | "default": true |
463 | }, | 477 | }, |
464 | "rust-analyzer.lens.run": { | 478 | "rust-analyzer.lens.run": { |
465 | "markdownDescription": "Whether to show Run lens. Only applies when `#rust-analyzer.lens.enable#` is set.", | 479 | "markdownDescription": "Whether to show `Run` lens. Only applies when `#rust-analyzer.lens.enable#` is set.", |
466 | "type": "boolean", | 480 | "type": "boolean", |
467 | "default": true | 481 | "default": true |
468 | }, | 482 | }, |
469 | "rust-analyzer.lens.debug": { | 483 | "rust-analyzer.lens.debug": { |
470 | "markdownDescription": "Whether to show Debug lens. Only applies when `#rust-analyzer.lens.enable#` is set.", | 484 | "markdownDescription": "Whether to show `Debug` lens. Only applies when `#rust-analyzer.lens.enable#` is set.", |
471 | "type": "boolean", | 485 | "type": "boolean", |
472 | "default": true | 486 | "default": true |
473 | }, | 487 | }, |
474 | "rust-analyzer.lens.implementations": { | 488 | "rust-analyzer.lens.implementations": { |
475 | "markdownDescription": "Whether to show Implementations lens. Only applies when `#rust-analyzer.lens.enable#` is set.", | 489 | "markdownDescription": "Whether to show `Implementations` lens. Only applies when `#rust-analyzer.lens.enable#` is set.", |
490 | "type": "boolean", | ||
491 | "default": true | ||
492 | }, | ||
493 | "rust-analyzer.hoverActions.enable": { | ||
494 | "description": "Whether to show HoverActions in Rust files.", | ||
495 | "type": "boolean", | ||
496 | "default": true | ||
497 | }, | ||
498 | "rust-analyzer.hoverActions.implementations": { | ||
499 | "markdownDescription": "Whether to show `Implementations` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.", | ||
500 | "type": "boolean", | ||
501 | "default": true | ||
502 | }, | ||
503 | "rust-analyzer.hoverActions.run": { | ||
504 | "markdownDescription": "Whether to show `Run` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.", | ||
505 | "type": "boolean", | ||
506 | "default": true | ||
507 | }, | ||
508 | "rust-analyzer.hoverActions.debug": { | ||
509 | "markdownDescription": "Whether to show `Debug` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.", | ||
510 | "type": "boolean", | ||
511 | "default": true | ||
512 | }, | ||
513 | "rust-analyzer.linkedProjects": { | ||
514 | "markdownDescription": "Disable project auto-discovery in favor of explicitly specified set of projects. \nElements must be paths pointing to Cargo.toml, rust-project.json, or JSON objects in rust-project.json format", | ||
515 | "type": "array", | ||
516 | "items": { | ||
517 | "type": [ | ||
518 | "string", | ||
519 | "object" | ||
520 | ] | ||
521 | }, | ||
522 | "default": null | ||
523 | }, | ||
524 | "rust-analyzer.withSysroot": { | ||
525 | "markdownDescription": "Internal config for debugging, disables loading of sysroot crates", | ||
476 | "type": "boolean", | 526 | "type": "boolean", |
477 | "default": true | 527 | "default": true |
478 | } | 528 | } |
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index d64f9a3f9..65ad573d8 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -1,8 +1,25 @@ | |||
1 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
2 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | import * as ra from '../src/lsp_ext'; | ||
4 | import * as Is from 'vscode-languageclient/lib/utils/is'; | ||
3 | 5 | ||
4 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; | 6 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; |
5 | import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; | 7 | import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; |
8 | import { assert } from './util'; | ||
9 | |||
10 | function renderCommand(cmd: ra.CommandLink) { | ||
11 | return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`; | ||
12 | } | ||
13 | |||
14 | function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString { | ||
15 | const text = actions.map(group => | ||
16 | (group.title ? (group.title + " ") : "") + group.commands.map(renderCommand).join(' | ') | ||
17 | ).join('___'); | ||
18 | |||
19 | const result = new vscode.MarkdownString(text); | ||
20 | result.isTrusted = true; | ||
21 | return result; | ||
22 | } | ||
6 | 23 | ||
7 | export function createClient(serverPath: string, cwd: string): lc.LanguageClient { | 24 | export function createClient(serverPath: string, cwd: string): lc.LanguageClient { |
8 | // '.' Is the fallback if no folder is open | 25 | // '.' Is the fallback if no folder is open |
@@ -32,6 +49,25 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
32 | if (res === undefined) throw new Error('busy'); | 49 | if (res === undefined) throw new Error('busy'); |
33 | return res; | 50 | return res; |
34 | }, | 51 | }, |
52 | async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _next: lc.ProvideHoverSignature) { | ||
53 | return client.sendRequest(lc.HoverRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token).then( | ||
54 | (result) => { | ||
55 | const hover = client.protocol2CodeConverter.asHover(result); | ||
56 | if (hover) { | ||
57 | const actions = (<any>result).actions; | ||
58 | if (actions) { | ||
59 | hover.contents.push(renderHoverActions(actions)); | ||
60 | } | ||
61 | } | ||
62 | return hover; | ||
63 | }, | ||
64 | (error) => { | ||
65 | client.logFailedRequest(lc.HoverRequest.type, error); | ||
66 | return Promise.resolve(null); | ||
67 | }); | ||
68 | }, | ||
69 | // Using custom handling of CodeActions where each code action is resloved lazily | ||
70 | // That's why we are not waiting for any command or edits | ||
35 | async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) { | 71 | async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) { |
36 | const params: lc.CodeActionParams = { | 72 | const params: lc.CodeActionParams = { |
37 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), | 73 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), |
@@ -43,32 +79,36 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
43 | const result: (vscode.CodeAction | vscode.Command)[] = []; | 79 | const result: (vscode.CodeAction | vscode.Command)[] = []; |
44 | const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>(); | 80 | const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>(); |
45 | for (const item of values) { | 81 | for (const item of values) { |
82 | // In our case we expect to get code edits only from diagnostics | ||
46 | if (lc.CodeAction.is(item)) { | 83 | if (lc.CodeAction.is(item)) { |
84 | assert(!item.command, "We don't expect to receive commands in CodeActions"); | ||
47 | const action = client.protocol2CodeConverter.asCodeAction(item); | 85 | const action = client.protocol2CodeConverter.asCodeAction(item); |
48 | const group = actionGroup(item); | 86 | result.push(action); |
49 | if (isSnippetEdit(item) || group) { | 87 | continue; |
50 | action.command = { | 88 | } |
51 | command: "rust-analyzer.applySnippetWorkspaceEdit", | 89 | assert(isCodeActionWithoutEditsAndCommands(item), "We don't expect edits or commands here"); |
52 | title: "", | 90 | const action = new vscode.CodeAction(item.title); |
53 | arguments: [action.edit], | 91 | const group = (item as any).group; |
54 | }; | 92 | const id = (item as any).id; |
55 | action.edit = undefined; | 93 | const resolveParams: ra.ResolveCodeActionParams = { |
56 | } | 94 | id: id, |
57 | 95 | codeActionParams: params | |
58 | if (group) { | 96 | }; |
59 | let entry = groups.get(group); | 97 | action.command = { |
60 | if (!entry) { | 98 | command: "rust-analyzer.resolveCodeAction", |
61 | entry = { index: result.length, items: [] }; | 99 | title: item.title, |
62 | groups.set(group, entry); | 100 | arguments: [resolveParams], |
63 | result.push(action); | 101 | }; |
64 | } | 102 | if (group) { |
65 | entry.items.push(action); | 103 | let entry = groups.get(group); |
66 | } else { | 104 | if (!entry) { |
105 | entry = { index: result.length, items: [] }; | ||
106 | groups.set(group, entry); | ||
67 | result.push(action); | 107 | result.push(action); |
68 | } | 108 | } |
109 | entry.items.push(action); | ||
69 | } else { | 110 | } else { |
70 | const command = client.protocol2CodeConverter.asCommand(item); | 111 | result.push(action); |
71 | result.push(command); | ||
72 | } | 112 | } |
73 | } | 113 | } |
74 | for (const [group, { index, items }] of groups) { | 114 | for (const [group, { index, items }] of groups) { |
@@ -80,7 +120,7 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient | |||
80 | command: "rust-analyzer.applyActionGroup", | 120 | command: "rust-analyzer.applyActionGroup", |
81 | title: "", | 121 | title: "", |
82 | arguments: [items.map((item) => { | 122 | arguments: [items.map((item) => { |
83 | return { label: item.title, edit: item.command!!.arguments!![0] }; | 123 | return { label: item.title, arguments: item.command!!.arguments!![0] }; |
84 | })], | 124 | })], |
85 | }; | 125 | }; |
86 | result[index] = action; | 126 | result[index] = action; |
@@ -119,24 +159,18 @@ class ExperimentalFeatures implements lc.StaticFeature { | |||
119 | const caps: any = capabilities.experimental ?? {}; | 159 | const caps: any = capabilities.experimental ?? {}; |
120 | caps.snippetTextEdit = true; | 160 | caps.snippetTextEdit = true; |
121 | caps.codeActionGroup = true; | 161 | caps.codeActionGroup = true; |
162 | caps.resolveCodeAction = true; | ||
163 | caps.hoverActions = true; | ||
122 | capabilities.experimental = caps; | 164 | capabilities.experimental = caps; |
123 | } | 165 | } |
124 | initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { | 166 | initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { |
125 | } | 167 | } |
126 | } | 168 | } |
127 | 169 | ||
128 | function isSnippetEdit(action: lc.CodeAction): boolean { | 170 | function isCodeActionWithoutEditsAndCommands(value: any): boolean { |
129 | const documentChanges = action.edit?.documentChanges ?? []; | 171 | const candidate: lc.CodeAction = value; |
130 | for (const edit of documentChanges) { | 172 | return candidate && Is.string(candidate.title) && |
131 | if (lc.TextDocumentEdit.is(edit)) { | 173 | (candidate.diagnostics === void 0 || Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) && |
132 | if (edit.edits.some((indel) => (indel as any).insertTextFormat === lc.InsertTextFormat.Snippet)) { | 174 | (candidate.kind === void 0 || Is.string(candidate.kind)) && |
133 | return true; | 175 | (candidate.edit === void 0 && candidate.command === void 0); |
134 | } | ||
135 | } | ||
136 | } | ||
137 | return false; | ||
138 | } | ||
139 | |||
140 | function actionGroup(action: lc.CodeAction): string | undefined { | ||
141 | return (action as any).group; | ||
142 | } | 176 | } |
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 86302db37..3e9c3aa0e 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts | |||
@@ -8,6 +8,7 @@ import { spawnSync } from 'child_process'; | |||
8 | import { RunnableQuickPick, selectRunnable, createTask } from './run'; | 8 | import { RunnableQuickPick, selectRunnable, createTask } from './run'; |
9 | import { AstInspector } from './ast_inspector'; | 9 | import { AstInspector } from './ast_inspector'; |
10 | import { isRustDocument, sleep, isRustEditor } from './util'; | 10 | import { isRustDocument, sleep, isRustEditor } from './util'; |
11 | import { startDebugSession, makeDebugConfig } from './debug'; | ||
11 | 12 | ||
12 | export * from './ast_inspector'; | 13 | export * from './ast_inspector'; |
13 | export * from './run'; | 14 | export * from './run'; |
@@ -197,20 +198,6 @@ export function toggleInlayHints(ctx: Ctx): Cmd { | |||
197 | }; | 198 | }; |
198 | } | 199 | } |
199 | 200 | ||
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 | 201 | // Opens the virtual file that will show the syntax tree |
215 | // | 202 | // |
216 | // The contents of the file come from the `TextDocumentContentProvider` | 203 | // The contents of the file come from the `TextDocumentContentProvider` |
@@ -356,10 +343,25 @@ export function showReferences(ctx: Ctx): Cmd { | |||
356 | } | 343 | } |
357 | 344 | ||
358 | export function applyActionGroup(_ctx: Ctx): Cmd { | 345 | export function applyActionGroup(_ctx: Ctx): Cmd { |
359 | return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => { | 346 | return async (actions: { label: string; arguments: ra.ResolveCodeActionParams }[]) => { |
360 | const selectedAction = await vscode.window.showQuickPick(actions); | 347 | const selectedAction = await vscode.window.showQuickPick(actions); |
361 | if (!selectedAction) return; | 348 | if (!selectedAction) return; |
362 | await applySnippetWorkspaceEdit(selectedAction.edit); | 349 | vscode.commands.executeCommand( |
350 | 'rust-analyzer.resolveCodeAction', | ||
351 | selectedAction.arguments, | ||
352 | ); | ||
353 | }; | ||
354 | } | ||
355 | |||
356 | export function resolveCodeAction(ctx: Ctx): Cmd { | ||
357 | const client = ctx.client; | ||
358 | return async (params: ra.ResolveCodeActionParams) => { | ||
359 | const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params); | ||
360 | if (!item) { | ||
361 | return; | ||
362 | } | ||
363 | const edit = client.protocol2CodeConverter.asWorkspaceEdit(item); | ||
364 | await applySnippetWorkspaceEdit(edit); | ||
363 | }; | 365 | }; |
364 | } | 366 | } |
365 | 367 | ||
@@ -368,3 +370,62 @@ export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd { | |||
368 | await applySnippetWorkspaceEdit(edit); | 370 | await applySnippetWorkspaceEdit(edit); |
369 | }; | 371 | }; |
370 | } | 372 | } |
373 | |||
374 | export function run(ctx: Ctx): Cmd { | ||
375 | let prevRunnable: RunnableQuickPick | undefined; | ||
376 | |||
377 | return async () => { | ||
378 | const item = await selectRunnable(ctx, prevRunnable); | ||
379 | if (!item) return; | ||
380 | |||
381 | item.detail = 'rerun'; | ||
382 | prevRunnable = item; | ||
383 | const task = createTask(item.runnable); | ||
384 | return await vscode.tasks.executeTask(task); | ||
385 | }; | ||
386 | } | ||
387 | |||
388 | export function runSingle(ctx: Ctx): Cmd { | ||
389 | return async (runnable: ra.Runnable) => { | ||
390 | const editor = ctx.activeRustEditor; | ||
391 | if (!editor) return; | ||
392 | |||
393 | const task = createTask(runnable); | ||
394 | task.group = vscode.TaskGroup.Build; | ||
395 | task.presentationOptions = { | ||
396 | reveal: vscode.TaskRevealKind.Always, | ||
397 | panel: vscode.TaskPanelKind.Dedicated, | ||
398 | clear: true, | ||
399 | }; | ||
400 | |||
401 | return vscode.tasks.executeTask(task); | ||
402 | }; | ||
403 | } | ||
404 | |||
405 | export function debug(ctx: Ctx): Cmd { | ||
406 | let prevDebuggee: RunnableQuickPick | undefined; | ||
407 | |||
408 | return async () => { | ||
409 | const item = await selectRunnable(ctx, prevDebuggee, true); | ||
410 | if (!item) return; | ||
411 | |||
412 | item.detail = 'restart'; | ||
413 | prevDebuggee = item; | ||
414 | return await startDebugSession(ctx, item.runnable); | ||
415 | }; | ||
416 | } | ||
417 | |||
418 | export function debugSingle(ctx: Ctx): Cmd { | ||
419 | return async (config: ra.Runnable) => { | ||
420 | await startDebugSession(ctx, config); | ||
421 | }; | ||
422 | } | ||
423 | |||
424 | export function newDebugConfig(ctx: Ctx): Cmd { | ||
425 | return async () => { | ||
426 | const item = await selectRunnable(ctx, undefined, true, false); | ||
427 | if (!item) return; | ||
428 | |||
429 | await makeDebugConfig(ctx, item.runnable); | ||
430 | }; | ||
431 | } | ||
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index e8abf8284..d8f0037d4 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -16,10 +16,8 @@ export class Config { | |||
16 | "files", | 16 | "files", |
17 | "highlighting", | 17 | "highlighting", |
18 | "updates.channel", | 18 | "updates.channel", |
19 | "lens.enable", | 19 | "lens", // works as lens.* |
20 | "lens.run", | 20 | "hoverActions", // works as hoverActions.* |
21 | "lens.debug", | ||
22 | "lens.implementations", | ||
23 | ] | 21 | ] |
24 | .map(opt => `${this.rootSection}.${opt}`); | 22 | .map(opt => `${this.rootSection}.${opt}`); |
25 | 23 | ||
@@ -132,4 +130,11 @@ export class Config { | |||
132 | implementations: this.get<boolean>("lens.implementations"), | 130 | implementations: this.get<boolean>("lens.implementations"), |
133 | }; | 131 | }; |
134 | } | 132 | } |
133 | |||
134 | get hoverActions() { | ||
135 | return { | ||
136 | enable: this.get<boolean>("hoverActions.enable"), | ||
137 | implementations: this.get<boolean>("hoverActions.implementations"), | ||
138 | }; | ||
139 | } | ||
135 | } | 140 | } |
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts index 027504ecd..a0c9b3ab2 100644 --- a/editors/code/src/debug.ts +++ b/editors/code/src/debug.ts | |||
@@ -3,46 +3,59 @@ import * as vscode from 'vscode'; | |||
3 | import * as path from 'path'; | 3 | import * as path from 'path'; |
4 | import * as ra from './lsp_ext'; | 4 | import * as ra from './lsp_ext'; |
5 | 5 | ||
6 | import { Cargo } from './cargo'; | 6 | import { Cargo } from './toolchain'; |
7 | import { Ctx } from "./ctx"; | 7 | import { Ctx } from "./ctx"; |
8 | 8 | ||
9 | const debugOutput = vscode.window.createOutputChannel("Debug"); | 9 | const debugOutput = vscode.window.createOutputChannel("Debug"); |
10 | type DebugConfigProvider = (config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>) => vscode.DebugConfiguration; | 10 | type DebugConfigProvider = (config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>) => vscode.DebugConfiguration; |
11 | 11 | ||
12 | function getLldbDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration { | 12 | export async function makeDebugConfig(ctx: Ctx, runnable: ra.Runnable): Promise<void> { |
13 | return { | 13 | const scope = ctx.activeRustEditor?.document.uri; |
14 | type: "lldb", | 14 | if (!scope) return; |
15 | request: "launch", | ||
16 | name: config.label, | ||
17 | program: executable, | ||
18 | args: config.extraArgs, | ||
19 | cwd: config.cwd, | ||
20 | sourceMap: sourceFileMap, | ||
21 | sourceLanguages: ["rust"] | ||
22 | }; | ||
23 | } | ||
24 | 15 | ||
25 | function getCppvsDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration { | 16 | const debugConfig = await getDebugConfiguration(ctx, runnable); |
26 | return { | 17 | if (!debugConfig) return; |
27 | type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg", | 18 | |
28 | request: "launch", | 19 | const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope); |
29 | name: config.label, | 20 | const configurations = wsLaunchSection.get<any[]>("configurations") || []; |
30 | program: executable, | 21 | |
31 | args: config.extraArgs, | 22 | const index = configurations.findIndex(c => c.name === debugConfig.name); |
32 | cwd: config.cwd, | 23 | if (index !== -1) { |
33 | sourceFileMap: sourceFileMap, | 24 | const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update'); |
34 | }; | 25 | if (answer === "Cancel") return; |
26 | |||
27 | configurations[index] = debugConfig; | ||
28 | } else { | ||
29 | configurations.push(debugConfig); | ||
30 | } | ||
31 | |||
32 | await wsLaunchSection.update("configurations", configurations); | ||
35 | } | 33 | } |
36 | 34 | ||
37 | async function getDebugExecutable(config: ra.Runnable): Promise<string> { | 35 | export async function startDebugSession(ctx: Ctx, runnable: ra.Runnable): Promise<boolean> { |
38 | const cargo = new Cargo(config.cwd || '.', debugOutput); | 36 | let debugConfig: vscode.DebugConfiguration | undefined = undefined; |
39 | const executable = await cargo.executableFromArgs(config.args); | 37 | let message = ""; |
40 | 38 | ||
41 | // if we are here, there were no compilation errors. | 39 | const wsLaunchSection = vscode.workspace.getConfiguration("launch"); |
42 | return executable; | 40 | const configurations = wsLaunchSection.get<any[]>("configurations") || []; |
41 | |||
42 | const index = configurations.findIndex(c => c.name === runnable.label); | ||
43 | if (-1 !== index) { | ||
44 | debugConfig = configurations[index]; | ||
45 | message = " (from launch.json)"; | ||
46 | debugOutput.clear(); | ||
47 | } else { | ||
48 | debugConfig = await getDebugConfiguration(ctx, runnable); | ||
49 | } | ||
50 | |||
51 | if (!debugConfig) return false; | ||
52 | |||
53 | debugOutput.appendLine(`Launching debug configuration${message}:`); | ||
54 | debugOutput.appendLine(JSON.stringify(debugConfig, null, 2)); | ||
55 | return vscode.debug.startDebugging(undefined, debugConfig); | ||
43 | } | 56 | } |
44 | 57 | ||
45 | export async function getDebugConfiguration(ctx: Ctx, config: ra.Runnable): Promise<vscode.DebugConfiguration | undefined> { | 58 | async function getDebugConfiguration(ctx: Ctx, runnable: ra.Runnable): Promise<vscode.DebugConfiguration | undefined> { |
46 | const editor = ctx.activeRustEditor; | 59 | const editor = ctx.activeRustEditor; |
47 | if (!editor) return; | 60 | if (!editor) return; |
48 | 61 | ||
@@ -78,8 +91,8 @@ export async function getDebugConfiguration(ctx: Ctx, config: ra.Runnable): Prom | |||
78 | return path.normalize(p).replace(wsFolder, '${workspaceRoot}'); | 91 | return path.normalize(p).replace(wsFolder, '${workspaceRoot}'); |
79 | } | 92 | } |
80 | 93 | ||
81 | const executable = await getDebugExecutable(config); | 94 | const executable = await getDebugExecutable(runnable); |
82 | const debugConfig = knownEngines[debugEngine.id](config, simplifyPath(executable), debugOptions.sourceFileMap); | 95 | const debugConfig = knownEngines[debugEngine.id](runnable, simplifyPath(executable), debugOptions.sourceFileMap); |
83 | if (debugConfig.type in debugOptions.engineSettings) { | 96 | if (debugConfig.type in debugOptions.engineSettings) { |
84 | const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; | 97 | const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; |
85 | for (var key in settingsMap) { | 98 | for (var key in settingsMap) { |
@@ -100,25 +113,35 @@ export async function getDebugConfiguration(ctx: Ctx, config: ra.Runnable): Prom | |||
100 | return debugConfig; | 113 | return debugConfig; |
101 | } | 114 | } |
102 | 115 | ||
103 | export async function startDebugSession(ctx: Ctx, config: ra.Runnable): Promise<boolean> { | 116 | async function getDebugExecutable(runnable: ra.Runnable): Promise<string> { |
104 | let debugConfig: vscode.DebugConfiguration | undefined = undefined; | 117 | const cargo = new Cargo(runnable.args.workspaceRoot || '.', debugOutput); |
105 | let message = ""; | 118 | const executable = await cargo.executableFromArgs(runnable.args.cargoArgs); |
106 | |||
107 | const wsLaunchSection = vscode.workspace.getConfiguration("launch"); | ||
108 | const configurations = wsLaunchSection.get<any[]>("configurations") || []; | ||
109 | 119 | ||
110 | const index = configurations.findIndex(c => c.name === config.label); | 120 | // if we are here, there were no compilation errors. |
111 | if (-1 !== index) { | 121 | return executable; |
112 | debugConfig = configurations[index]; | 122 | } |
113 | message = " (from launch.json)"; | ||
114 | debugOutput.clear(); | ||
115 | } else { | ||
116 | debugConfig = await getDebugConfiguration(ctx, config); | ||
117 | } | ||
118 | 123 | ||
119 | if (!debugConfig) return false; | 124 | function getLldbDebugConfig(runnable: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration { |
125 | return { | ||
126 | type: "lldb", | ||
127 | request: "launch", | ||
128 | name: runnable.label, | ||
129 | program: executable, | ||
130 | args: runnable.args.executableArgs, | ||
131 | cwd: runnable.args.workspaceRoot, | ||
132 | sourceMap: sourceFileMap, | ||
133 | sourceLanguages: ["rust"] | ||
134 | }; | ||
135 | } | ||
120 | 136 | ||
121 | debugOutput.appendLine(`Launching debug configuration${message}:`); | 137 | function getCppvsDebugConfig(runnable: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration { |
122 | debugOutput.appendLine(JSON.stringify(debugConfig, null, 2)); | 138 | return { |
123 | return vscode.debug.startDebugging(undefined, debugConfig); | 139 | type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg", |
140 | request: "launch", | ||
141 | name: runnable.label, | ||
142 | program: executable, | ||
143 | args: runnable.args.executableArgs, | ||
144 | cwd: runnable.args.workspaceRoot, | ||
145 | sourceFileMap: sourceFileMap, | ||
146 | }; | ||
124 | } | 147 | } |
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 4da12eb30..e16ea799c 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts | |||
@@ -33,6 +33,12 @@ export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position | |||
33 | 33 | ||
34 | export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule"); | 34 | export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule"); |
35 | 35 | ||
36 | export interface ResolveCodeActionParams { | ||
37 | id: string; | ||
38 | codeActionParams: lc.CodeActionParams; | ||
39 | } | ||
40 | export const resolveCodeAction = new lc.RequestType<ResolveCodeActionParams, lc.WorkspaceEdit, unknown>('experimental/resolveCodeAction'); | ||
41 | |||
36 | export interface JoinLinesParams { | 42 | export interface JoinLinesParams { |
37 | textDocument: lc.TextDocumentIdentifier; | 43 | textDocument: lc.TextDocumentIdentifier; |
38 | ranges: lc.Range[]; | 44 | ranges: lc.Range[]; |
@@ -45,16 +51,18 @@ export interface RunnablesParams { | |||
45 | textDocument: lc.TextDocumentIdentifier; | 51 | textDocument: lc.TextDocumentIdentifier; |
46 | position: lc.Position | null; | 52 | position: lc.Position | null; |
47 | } | 53 | } |
54 | |||
48 | export interface Runnable { | 55 | export interface Runnable { |
49 | range: lc.Range; | ||
50 | label: string; | 56 | label: string; |
51 | bin: string; | 57 | location?: lc.LocationLink; |
52 | args: string[]; | 58 | kind: "cargo"; |
53 | extraArgs: string[]; | 59 | args: { |
54 | env: { [key: string]: string }; | 60 | workspaceRoot?: string; |
55 | cwd: string | null; | 61 | cargoArgs: string[]; |
62 | executableArgs: string[]; | ||
63 | }; | ||
56 | } | 64 | } |
57 | export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("rust-analyzer/runnables"); | 65 | export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables"); |
58 | 66 | ||
59 | export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint; | 67 | export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint; |
60 | 68 | ||
@@ -82,3 +90,15 @@ export interface SsrParams { | |||
82 | parseOnly: boolean; | 90 | parseOnly: boolean; |
83 | } | 91 | } |
84 | export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr'); | 92 | export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr'); |
93 | |||
94 | export interface CommandLink extends lc.Command { | ||
95 | /** | ||
96 | * A tooltip for the command, when represented in the UI. | ||
97 | */ | ||
98 | tooltip?: string; | ||
99 | } | ||
100 | |||
101 | export interface CommandLinkGroup { | ||
102 | title?: string; | ||
103 | commands: CommandLink[]; | ||
104 | } | ||
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index b7337621c..a92c676fa 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts | |||
@@ -98,6 +98,7 @@ export async function activate(context: vscode.ExtensionContext) { | |||
98 | ctx.registerCommand('debugSingle', commands.debugSingle); | 98 | ctx.registerCommand('debugSingle', commands.debugSingle); |
99 | ctx.registerCommand('showReferences', commands.showReferences); | 99 | ctx.registerCommand('showReferences', commands.showReferences); |
100 | ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); | 100 | ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); |
101 | ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction); | ||
101 | ctx.registerCommand('applyActionGroup', commands.applyActionGroup); | 102 | ctx.registerCommand('applyActionGroup', commands.applyActionGroup); |
102 | 103 | ||
103 | ctx.pushCleanup(activateTaskProvider(workspaceFolder)); | 104 | ctx.pushCleanup(activateTaskProvider(workspaceFolder)); |
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts index 8e1ba83ed..bb060cfe1 100644 --- a/editors/code/src/run.ts +++ b/editors/code/src/run.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as lc from 'vscode-languageclient'; | 2 | import * as lc from 'vscode-languageclient'; |
3 | import * as ra from './lsp_ext'; | 3 | import * as ra from './lsp_ext'; |
4 | import * as toolchain from "./toolchain"; | ||
4 | 5 | ||
5 | import { Ctx, Cmd } from './ctx'; | 6 | import { Ctx } from './ctx'; |
6 | import { startDebugSession, getDebugConfiguration } from './debug'; | 7 | import { makeDebugConfig } from './debug'; |
7 | 8 | ||
8 | const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; | 9 | const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; |
9 | 10 | ||
@@ -64,7 +65,7 @@ export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, | |||
64 | quickPick.onDidHide(() => close()), | 65 | quickPick.onDidHide(() => close()), |
65 | quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), | 66 | quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), |
66 | quickPick.onDidTriggerButton((_button) => { | 67 | quickPick.onDidTriggerButton((_button) => { |
67 | (async () => await makeDebugConfig(ctx, quickPick.activeItems[0]))(); | 68 | (async () => await makeDebugConfig(ctx, quickPick.activeItems[0].runnable))(); |
68 | close(); | 69 | close(); |
69 | }), | 70 | }), |
70 | quickPick.onDidChangeActive((active) => { | 71 | quickPick.onDidChangeActive((active) => { |
@@ -83,74 +84,6 @@ export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, | |||
83 | }); | 84 | }); |
84 | } | 85 | } |
85 | 86 | ||
86 | export function runSingle(ctx: Ctx): Cmd { | ||
87 | return async (runnable: ra.Runnable) => { | ||
88 | const editor = ctx.activeRustEditor; | ||
89 | if (!editor) return; | ||
90 | |||
91 | const task = createTask(runnable); | ||
92 | task.group = vscode.TaskGroup.Build; | ||
93 | task.presentationOptions = { | ||
94 | reveal: vscode.TaskRevealKind.Always, | ||
95 | panel: vscode.TaskPanelKind.Dedicated, | ||
96 | clear: true, | ||
97 | }; | ||
98 | |||
99 | return vscode.tasks.executeTask(task); | ||
100 | }; | ||
101 | } | ||
102 | |||
103 | export function debug(ctx: Ctx): Cmd { | ||
104 | let prevDebuggee: RunnableQuickPick | undefined; | ||
105 | |||
106 | return async () => { | ||
107 | const item = await selectRunnable(ctx, prevDebuggee, true); | ||
108 | if (!item) return; | ||
109 | |||
110 | item.detail = 'restart'; | ||
111 | prevDebuggee = item; | ||
112 | return await startDebugSession(ctx, item.runnable); | ||
113 | }; | ||
114 | } | ||
115 | |||
116 | export function debugSingle(ctx: Ctx): Cmd { | ||
117 | return async (config: ra.Runnable) => { | ||
118 | await startDebugSession(ctx, config); | ||
119 | }; | ||
120 | } | ||
121 | |||
122 | async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise<void> { | ||
123 | const scope = ctx.activeRustEditor?.document.uri; | ||
124 | if (!scope) return; | ||
125 | |||
126 | const debugConfig = await getDebugConfiguration(ctx, item.runnable); | ||
127 | if (!debugConfig) return; | ||
128 | |||
129 | const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope); | ||
130 | const configurations = wsLaunchSection.get<any[]>("configurations") || []; | ||
131 | |||
132 | const index = configurations.findIndex(c => c.name === debugConfig.name); | ||
133 | if (index !== -1) { | ||
134 | const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update'); | ||
135 | if (answer === "Cancel") return; | ||
136 | |||
137 | configurations[index] = debugConfig; | ||
138 | } else { | ||
139 | configurations.push(debugConfig); | ||
140 | } | ||
141 | |||
142 | await wsLaunchSection.update("configurations", configurations); | ||
143 | } | ||
144 | |||
145 | export function newDebugConfig(ctx: Ctx): Cmd { | ||
146 | return async () => { | ||
147 | const item = await selectRunnable(ctx, undefined, true, false); | ||
148 | if (!item) return; | ||
149 | |||
150 | await makeDebugConfig(ctx, item); | ||
151 | }; | ||
152 | } | ||
153 | |||
154 | export class RunnableQuickPick implements vscode.QuickPickItem { | 87 | export class RunnableQuickPick implements vscode.QuickPickItem { |
155 | public label: string; | 88 | public label: string; |
156 | public description?: string | undefined; | 89 | public description?: string | undefined; |
@@ -170,18 +103,27 @@ interface CargoTaskDefinition extends vscode.TaskDefinition { | |||
170 | env?: { [key: string]: string }; | 103 | env?: { [key: string]: string }; |
171 | } | 104 | } |
172 | 105 | ||
173 | export function createTask(spec: ra.Runnable): vscode.Task { | 106 | export function createTask(runnable: ra.Runnable): vscode.Task { |
174 | const TASK_SOURCE = 'Rust'; | 107 | const TASK_SOURCE = 'Rust'; |
108 | |||
109 | let command; | ||
110 | switch (runnable.kind) { | ||
111 | case "cargo": command = toolchain.getPathForExecutable("cargo"); | ||
112 | } | ||
113 | const args = [...runnable.args.cargoArgs]; // should be a copy! | ||
114 | if (runnable.args.executableArgs.length > 0) { | ||
115 | args.push('--', ...runnable.args.executableArgs); | ||
116 | } | ||
175 | const definition: CargoTaskDefinition = { | 117 | const definition: CargoTaskDefinition = { |
176 | type: 'cargo', | 118 | type: 'cargo', |
177 | label: spec.label, | 119 | label: runnable.label, |
178 | command: spec.bin, | 120 | command, |
179 | args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args, | 121 | args, |
180 | env: Object.assign({}, process.env, spec.env), | 122 | env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }), |
181 | }; | 123 | }; |
182 | 124 | ||
183 | const execOption: vscode.ShellExecutionOptions = { | 125 | const execOption: vscode.ShellExecutionOptions = { |
184 | cwd: spec.cwd || '.', | 126 | cwd: runnable.args.workspaceRoot || '.', |
185 | env: definition.env, | 127 | env: definition.env, |
186 | }; | 128 | }; |
187 | const exec = new vscode.ShellExecution( | 129 | const exec = new vscode.ShellExecution( |
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts index 1366c76d6..9748824df 100644 --- a/editors/code/src/tasks.ts +++ b/editors/code/src/tasks.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as toolchain from "./toolchain"; | ||
2 | 3 | ||
3 | // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and | 4 | // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and |
4 | // our configuration should be compatible with it so use the same key. | 5 | // our configuration should be compatible with it so use the same key. |
@@ -24,6 +25,8 @@ class CargoTaskProvider implements vscode.TaskProvider { | |||
24 | // set of tasks that always exist. These tasks cannot be removed in | 25 | // set of tasks that always exist. These tasks cannot be removed in |
25 | // tasks.json - only tweaked. | 26 | // tasks.json - only tweaked. |
26 | 27 | ||
28 | const cargoPath = toolchain.cargoPath(); | ||
29 | |||
27 | return [ | 30 | return [ |
28 | { command: 'build', group: vscode.TaskGroup.Build }, | 31 | { command: 'build', group: vscode.TaskGroup.Build }, |
29 | { command: 'check', group: vscode.TaskGroup.Build }, | 32 | { command: 'check', group: vscode.TaskGroup.Build }, |
@@ -46,7 +49,7 @@ class CargoTaskProvider implements vscode.TaskProvider { | |||
46 | `cargo ${command}`, | 49 | `cargo ${command}`, |
47 | 'rust', | 50 | 'rust', |
48 | // What to do when this command is executed. | 51 | // What to do when this command is executed. |
49 | new vscode.ShellExecution('cargo', [command]), | 52 | new vscode.ShellExecution(cargoPath, [command]), |
50 | // Problem matchers. | 53 | // Problem matchers. |
51 | ['$rustc'], | 54 | ['$rustc'], |
52 | ); | 55 | ); |
@@ -80,4 +83,4 @@ class CargoTaskProvider implements vscode.TaskProvider { | |||
80 | export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { | 83 | export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { |
81 | const provider = new CargoTaskProvider(target); | 84 | const provider = new CargoTaskProvider(target); |
82 | return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); | 85 | return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); |
83 | } \ No newline at end of file | 86 | } |
diff --git a/editors/code/src/cargo.ts b/editors/code/src/toolchain.ts index a55b2f860..80a7915e9 100644 --- a/editors/code/src/cargo.ts +++ b/editors/code/src/toolchain.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import * as cp from 'child_process'; | 1 | import * as cp from 'child_process'; |
2 | import * as os from 'os'; | 2 | import * as os from 'os'; |
3 | import * as path from 'path'; | 3 | import * as path from 'path'; |
4 | import * as fs from 'fs'; | ||
4 | import * as readline from 'readline'; | 5 | import * as readline from 'readline'; |
5 | import { OutputChannel } from 'vscode'; | 6 | import { OutputChannel } from 'vscode'; |
6 | import { isValidExecutable } from './util'; | 7 | import { log, memoize } from './util'; |
7 | 8 | ||
8 | interface CompilationArtifact { | 9 | interface CompilationArtifact { |
9 | fileName: string; | 10 | fileName: string; |
@@ -17,33 +18,34 @@ export interface ArtifactSpec { | |||
17 | filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[]; | 18 | filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[]; |
18 | } | 19 | } |
19 | 20 | ||
20 | export function artifactSpec(args: readonly string[]): ArtifactSpec { | 21 | export class Cargo { |
21 | const cargoArgs = [...args, "--message-format=json"]; | 22 | constructor(readonly rootFolder: string, readonly output: OutputChannel) { } |
22 | 23 | ||
23 | // arguments for a runnable from the quick pick should be updated. | 24 | // Made public for testing purposes |
24 | // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens | 25 | static artifactSpec(args: readonly string[]): ArtifactSpec { |
25 | switch (cargoArgs[0]) { | 26 | const cargoArgs = [...args, "--message-format=json"]; |
26 | case "run": cargoArgs[0] = "build"; break; | 27 | |
27 | case "test": { | 28 | // arguments for a runnable from the quick pick should be updated. |
28 | if (!cargoArgs.includes("--no-run")) { | 29 | // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens |
29 | cargoArgs.push("--no-run"); | 30 | switch (cargoArgs[0]) { |
31 | case "run": cargoArgs[0] = "build"; break; | ||
32 | case "test": { | ||
33 | if (!cargoArgs.includes("--no-run")) { | ||
34 | cargoArgs.push("--no-run"); | ||
35 | } | ||
36 | break; | ||
30 | } | 37 | } |
31 | break; | ||
32 | } | 38 | } |
33 | } | ||
34 | 39 | ||
35 | const result: ArtifactSpec = { cargoArgs: cargoArgs }; | 40 | const result: ArtifactSpec = { cargoArgs: cargoArgs }; |
36 | if (cargoArgs[0] === "test") { | 41 | if (cargoArgs[0] === "test") { |
37 | // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests | 42 | // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests |
38 | // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} | 43 | // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} |
39 | result.filter = (artifacts) => artifacts.filter(it => it.isTest); | 44 | result.filter = (artifacts) => artifacts.filter(it => it.isTest); |
40 | } | 45 | } |
41 | |||
42 | return result; | ||
43 | } | ||
44 | 46 | ||
45 | export class Cargo { | 47 | return result; |
46 | constructor(readonly rootFolder: string, readonly output: OutputChannel) { } | 48 | } |
47 | 49 | ||
48 | private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> { | 50 | private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> { |
49 | const artifacts: CompilationArtifact[] = []; | 51 | const artifacts: CompilationArtifact[] = []; |
@@ -77,7 +79,7 @@ export class Cargo { | |||
77 | } | 79 | } |
78 | 80 | ||
79 | async executableFromArgs(args: readonly string[]): Promise<string> { | 81 | async executableFromArgs(args: readonly string[]): Promise<string> { |
80 | const artifacts = await this.getArtifacts(artifactSpec(args)); | 82 | const artifacts = await this.getArtifacts(Cargo.artifactSpec(args)); |
81 | 83 | ||
82 | if (artifacts.length === 0) { | 84 | if (artifacts.length === 0) { |
83 | throw new Error('No compilation artifacts'); | 85 | throw new Error('No compilation artifacts'); |
@@ -94,14 +96,7 @@ export class Cargo { | |||
94 | onStderrString: (data: string) => void | 96 | onStderrString: (data: string) => void |
95 | ): Promise<number> { | 97 | ): Promise<number> { |
96 | return new Promise((resolve, reject) => { | 98 | return new Promise((resolve, reject) => { |
97 | let cargoPath; | 99 | const cargo = cp.spawn(cargoPath(), cargoArgs, { |
98 | try { | ||
99 | cargoPath = getCargoPathOrFail(); | ||
100 | } catch (err) { | ||
101 | return reject(err); | ||
102 | } | ||
103 | |||
104 | const cargo = cp.spawn(cargoPath, cargoArgs, { | ||
105 | stdio: ['ignore', 'pipe', 'pipe'], | 100 | stdio: ['ignore', 'pipe', 'pipe'], |
106 | cwd: this.rootFolder | 101 | cwd: this.rootFolder |
107 | }); | 102 | }); |
@@ -126,26 +121,54 @@ export class Cargo { | |||
126 | } | 121 | } |
127 | } | 122 | } |
128 | 123 | ||
129 | // Mirrors `ra_env::get_path_for_executable` implementation | 124 | /** Mirrors `ra_toolchain::cargo()` implementation */ |
130 | function getCargoPathOrFail(): string { | 125 | export function cargoPath(): string { |
131 | const envVar = process.env.CARGO; | 126 | return getPathForExecutable("cargo"); |
132 | const executableName = "cargo"; | 127 | } |
128 | |||
129 | /** Mirrors `ra_toolchain::get_path_for_executable()` implementation */ | ||
130 | export const getPathForExecutable = memoize( | ||
131 | // We apply caching to decrease file-system interactions | ||
132 | (executableName: "cargo" | "rustc" | "rustup"): string => { | ||
133 | { | ||
134 | const envVar = process.env[executableName.toUpperCase()]; | ||
135 | if (envVar) return envVar; | ||
136 | } | ||
137 | |||
138 | if (lookupInPath(executableName)) return executableName; | ||
133 | 139 | ||
134 | if (envVar) { | 140 | try { |
135 | if (isValidExecutable(envVar)) return envVar; | 141 | // hmm, `os.homedir()` seems to be infallible |
142 | // it is not mentioned in docs and cannot be infered by the type signature... | ||
143 | const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName); | ||
136 | 144 | ||
137 | throw new Error(`\`${envVar}\` environment variable points to something that's not a valid executable`); | 145 | if (isFile(standardPath)) return standardPath; |
146 | } catch (err) { | ||
147 | log.error("Failed to read the fs info", err); | ||
148 | } | ||
149 | return executableName; | ||
138 | } | 150 | } |
151 | ); | ||
139 | 152 | ||
140 | if (isValidExecutable(executableName)) return executableName; | 153 | function lookupInPath(exec: string): boolean { |
154 | const paths = process.env.PATH ?? "";; | ||
141 | 155 | ||
142 | const standardLocation = path.join(os.homedir(), '.cargo', 'bin', executableName); | 156 | const candidates = paths.split(path.delimiter).flatMap(dirInPath => { |
157 | const candidate = path.join(dirInPath, exec); | ||
158 | return os.type() === "Windows_NT" | ||
159 | ? [candidate, `${candidate}.exe`] | ||
160 | : [candidate]; | ||
161 | }); | ||
143 | 162 | ||
144 | if (isValidExecutable(standardLocation)) return standardLocation; | 163 | return candidates.some(isFile); |
164 | } | ||
145 | 165 | ||
146 | throw new Error( | 166 | function isFile(suspectPath: string): boolean { |
147 | `Failed to find \`${executableName}\` executable. ` + | 167 | // It is not mentionned in docs, but `statSync()` throws an error when |
148 | `Make sure \`${executableName}\` is in \`$PATH\`, ` + | 168 | // the path doesn't exist |
149 | `or set \`${envVar}\` to point to a valid executable.` | 169 | try { |
150 | ); | 170 | return fs.statSync(suspectPath).isFile(); |
171 | } catch { | ||
172 | return false; | ||
173 | } | ||
151 | } | 174 | } |
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 352ef9162..fe3fb71cd 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts | |||
@@ -99,3 +99,21 @@ export function isValidExecutable(path: string): boolean { | |||
99 | export function setContextValue(key: string, value: any): Thenable<void> { | 99 | export function setContextValue(key: string, value: any): Thenable<void> { |
100 | return vscode.commands.executeCommand('setContext', key, value); | 100 | return vscode.commands.executeCommand('setContext', key, value); |
101 | } | 101 | } |
102 | |||
103 | /** | ||
104 | * Returns a higher-order function that caches the results of invoking the | ||
105 | * underlying function. | ||
106 | */ | ||
107 | export function memoize<Ret, TThis, Param extends string>(func: (this: TThis, arg: Param) => Ret) { | ||
108 | const cache = new Map<string, Ret>(); | ||
109 | |||
110 | return function(this: TThis, arg: Param) { | ||
111 | const cached = cache.get(arg); | ||
112 | if (cached) return cached; | ||
113 | |||
114 | const result = func.call(this, arg); | ||
115 | cache.set(arg, result); | ||
116 | |||
117 | return result; | ||
118 | }; | ||
119 | } | ||
diff --git a/editors/code/tests/unit/launch_config.test.ts b/editors/code/tests/unit/launch_config.test.ts index d5cf1b74e..68794d53e 100644 --- a/editors/code/tests/unit/launch_config.test.ts +++ b/editors/code/tests/unit/launch_config.test.ts | |||
@@ -1,25 +1,25 @@ | |||
1 | import * as assert from 'assert'; | 1 | import * as assert from 'assert'; |
2 | import * as cargo from '../../src/cargo'; | 2 | import { Cargo } from '../../src/toolchain'; |
3 | 3 | ||
4 | suite('Launch configuration', () => { | 4 | suite('Launch configuration', () => { |
5 | 5 | ||
6 | suite('Lens', () => { | 6 | suite('Lens', () => { |
7 | test('A binary', async () => { | 7 | test('A binary', async () => { |
8 | const args = cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "pkg_name"]); | 8 | const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "pkg_name"]); |
9 | 9 | ||
10 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); | 10 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); |
11 | assert.deepEqual(args.filter, undefined); | 11 | assert.deepEqual(args.filter, undefined); |
12 | }); | 12 | }); |
13 | 13 | ||
14 | test('One of Multiple Binaries', async () => { | 14 | test('One of Multiple Binaries', async () => { |
15 | const args = cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "bin1"]); | 15 | const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "bin1"]); |
16 | 16 | ||
17 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin1", "--message-format=json"]); | 17 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin1", "--message-format=json"]); |
18 | assert.deepEqual(args.filter, undefined); | 18 | assert.deepEqual(args.filter, undefined); |
19 | }); | 19 | }); |
20 | 20 | ||
21 | test('A test', async () => { | 21 | test('A test', async () => { |
22 | const args = cargo.artifactSpec(["test", "--package", "pkg_name", "--lib", "--no-run"]); | 22 | const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib", "--no-run"]); |
23 | 23 | ||
24 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--no-run", "--message-format=json"]); | 24 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--no-run", "--message-format=json"]); |
25 | assert.notDeepEqual(args.filter, undefined); | 25 | assert.notDeepEqual(args.filter, undefined); |
@@ -28,7 +28,7 @@ suite('Launch configuration', () => { | |||
28 | 28 | ||
29 | suite('QuickPick', () => { | 29 | suite('QuickPick', () => { |
30 | test('A binary', async () => { | 30 | test('A binary', async () => { |
31 | const args = cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "pkg_name"]); | 31 | const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "pkg_name"]); |
32 | 32 | ||
33 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); | 33 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); |
34 | assert.deepEqual(args.filter, undefined); | 34 | assert.deepEqual(args.filter, undefined); |
@@ -36,14 +36,14 @@ suite('Launch configuration', () => { | |||
36 | 36 | ||
37 | 37 | ||
38 | test('One of Multiple Binaries', async () => { | 38 | test('One of Multiple Binaries', async () => { |
39 | const args = cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "bin2"]); | 39 | const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "bin2"]); |
40 | 40 | ||
41 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin2", "--message-format=json"]); | 41 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin2", "--message-format=json"]); |
42 | assert.deepEqual(args.filter, undefined); | 42 | assert.deepEqual(args.filter, undefined); |
43 | }); | 43 | }); |
44 | 44 | ||
45 | test('A test', async () => { | 45 | test('A test', async () => { |
46 | const args = cargo.artifactSpec(["test", "--package", "pkg_name", "--lib"]); | 46 | const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib"]); |
47 | 47 | ||
48 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--message-format=json", "--no-run"]); | 48 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--message-format=json", "--no-run"]); |
49 | assert.notDeepEqual(args.filter, undefined); | 49 | assert.notDeepEqual(args.filter, undefined); |