aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/cargo.ts100
-rw-r--r--editors/code/src/client.ts65
-rw-r--r--editors/code/src/color_theme.ts129
-rw-r--r--editors/code/src/commands/index.ts56
-rw-r--r--editors/code/src/commands/join_lines.ts12
-rw-r--r--editors/code/src/commands/on_enter.ts5
-rw-r--r--editors/code/src/commands/runnables.ts197
-rw-r--r--editors/code/src/commands/ssr.ts2
-rw-r--r--editors/code/src/commands/syntax_tree.ts4
-rw-r--r--editors/code/src/config.ts20
-rw-r--r--editors/code/src/debug.ts124
-rw-r--r--editors/code/src/inlay_hints.ts14
-rw-r--r--editors/code/src/main.ts11
-rw-r--r--editors/code/src/rust-analyzer-api.ts6
-rw-r--r--editors/code/src/util.ts11
15 files changed, 481 insertions, 275 deletions
diff --git a/editors/code/src/cargo.ts b/editors/code/src/cargo.ts
index a328ba9bd..6a41873d0 100644
--- a/editors/code/src/cargo.ts
+++ b/editors/code/src/cargo.ts
@@ -1,6 +1,9 @@
1import * as cp from 'child_process'; 1import * as cp from 'child_process';
2import * as os from 'os';
3import * as path from 'path';
2import * as readline from 'readline'; 4import * as readline from 'readline';
3import { OutputChannel } from 'vscode'; 5import { OutputChannel } from 'vscode';
6import { isValidExecutable } from './util';
4 7
5interface CompilationArtifact { 8interface CompilationArtifact {
6 fileName: string; 9 fileName: string;
@@ -10,17 +13,9 @@ interface CompilationArtifact {
10} 13}
11 14
12export class Cargo { 15export class Cargo {
13 rootFolder: string; 16 constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
14 env?: Record<string, string>;
15 output: OutputChannel;
16
17 public constructor(cargoTomlFolder: string, output: OutputChannel, env: Record<string, string> | undefined = undefined) {
18 this.rootFolder = cargoTomlFolder;
19 this.output = output;
20 this.env = env;
21 }
22 17
23 public async artifactsFromArgs(cargoArgs: string[]): Promise<CompilationArtifact[]> { 18 private async artifactsFromArgs(cargoArgs: string[]): Promise<CompilationArtifact[]> {
24 const artifacts: CompilationArtifact[] = []; 19 const artifacts: CompilationArtifact[] = [];
25 20
26 try { 21 try {
@@ -37,17 +32,13 @@ export class Cargo {
37 isTest: message.profile.test 32 isTest: message.profile.test
38 }); 33 });
39 } 34 }
40 } 35 } else if (message.reason === 'compiler-message') {
41 else if (message.reason === 'compiler-message') {
42 this.output.append(message.message.rendered); 36 this.output.append(message.message.rendered);
43 } 37 }
44 }, 38 },
45 stderr => { 39 stderr => this.output.append(stderr),
46 this.output.append(stderr);
47 }
48 ); 40 );
49 } 41 } catch (err) {
50 catch (err) {
51 this.output.show(true); 42 this.output.show(true);
52 throw new Error(`Cargo invocation has failed: ${err}`); 43 throw new Error(`Cargo invocation has failed: ${err}`);
53 } 44 }
@@ -55,11 +46,27 @@ export class Cargo {
55 return artifacts; 46 return artifacts;
56 } 47 }
57 48
58 public async executableFromArgs(args: string[]): Promise<string> { 49 async executableFromArgs(args: readonly string[]): Promise<string> {
59 const cargoArgs = [...args]; // to remain args unchanged 50 const cargoArgs = [...args, "--message-format=json"];
60 cargoArgs.push("--message-format=json");
61 51
62 const artifacts = await this.artifactsFromArgs(cargoArgs); 52 // arguments for a runnable from the quick pick should be updated.
53 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
54 switch (cargoArgs[0]) {
55 case "run": cargoArgs[0] = "build"; break;
56 case "test": {
57 if (cargoArgs.indexOf("--no-run") === -1) {
58 cargoArgs.push("--no-run");
59 }
60 break;
61 }
62 }
63
64 let artifacts = await this.artifactsFromArgs(cargoArgs);
65 if (cargoArgs[0] === "test") {
66 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
67 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
68 artifacts = artifacts.filter(a => a.isTest);
69 }
63 70
64 if (artifacts.length === 0) { 71 if (artifacts.length === 0) {
65 throw new Error('No compilation artifacts'); 72 throw new Error('No compilation artifacts');
@@ -70,24 +77,27 @@ export class Cargo {
70 return artifacts[0].fileName; 77 return artifacts[0].fileName;
71 } 78 }
72 79
73 runCargo( 80 private runCargo(
74 cargoArgs: string[], 81 cargoArgs: string[],
75 onStdoutJson: (obj: any) => void, 82 onStdoutJson: (obj: any) => void,
76 onStderrString: (data: string) => void 83 onStderrString: (data: string) => void
77 ): Promise<number> { 84 ): Promise<number> {
78 return new Promise<number>((resolve, reject) => { 85 return new Promise((resolve, reject) => {
79 const cargo = cp.spawn('cargo', cargoArgs, { 86 let cargoPath;
87 try {
88 cargoPath = getCargoPathOrFail();
89 } catch (err) {
90 return reject(err);
91 }
92
93 const cargo = cp.spawn(cargoPath, cargoArgs, {
80 stdio: ['ignore', 'pipe', 'pipe'], 94 stdio: ['ignore', 'pipe', 'pipe'],
81 cwd: this.rootFolder, 95 cwd: this.rootFolder
82 env: this.env,
83 }); 96 });
84 97
85 cargo.on('error', err => { 98 cargo.on('error', err => reject(new Error(`could not launch cargo: ${err}`)));
86 reject(new Error(`could not launch cargo: ${err}`)); 99
87 }); 100 cargo.stderr.on('data', chunk => onStderrString(chunk.toString()));
88 cargo.stderr.on('data', chunk => {
89 onStderrString(chunk.toString());
90 });
91 101
92 const rl = readline.createInterface({ input: cargo.stdout }); 102 const rl = readline.createInterface({ input: cargo.stdout });
93 rl.on('line', line => { 103 rl.on('line', line => {
@@ -103,4 +113,28 @@ export class Cargo {
103 }); 113 });
104 }); 114 });
105 } 115 }
106} \ No newline at end of file 116}
117
118// Mirrors `ra_env::get_path_for_executable` implementation
119function getCargoPathOrFail(): string {
120 const envVar = process.env.CARGO;
121 const executableName = "cargo";
122
123 if (envVar) {
124 if (isValidExecutable(envVar)) return envVar;
125
126 throw new Error(`\`${envVar}\` environment variable points to something that's not a valid executable`);
127 }
128
129 if (isValidExecutable(executableName)) return executableName;
130
131 const standardLocation = path.join(os.homedir(), '.cargo', 'bin', executableName);
132
133 if (isValidExecutable(standardLocation)) return standardLocation;
134
135 throw new Error(
136 `Failed to find \`${executableName}\` executable. ` +
137 `Make sure \`${executableName}\` is in \`$PATH\`, ` +
138 `or set \`${envVar}\` to point to a valid executable.`
139 );
140}
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index cffdcf11a..fac1a0be3 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -31,24 +31,79 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
31 const res = await next(document, token); 31 const res = await next(document, token);
32 if (res === undefined) throw new Error('busy'); 32 if (res === undefined) throw new Error('busy');
33 return res; 33 return res;
34 },
35 async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
36 const params: lc.CodeActionParams = {
37 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
38 range: client.code2ProtocolConverter.asRange(range),
39 context: client.code2ProtocolConverter.asCodeActionContext(context)
40 };
41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
42 if (values === null) return undefined;
43 const result: (vscode.CodeAction | vscode.Command)[] = [];
44 for (const item of values) {
45 if (lc.CodeAction.is(item)) {
46 const action = client.protocol2CodeConverter.asCodeAction(item);
47 if (isSnippetEdit(item)) {
48 action.command = {
49 command: "rust-analyzer.applySnippetWorkspaceEdit",
50 title: "",
51 arguments: [action.edit],
52 };
53 action.edit = undefined;
54 }
55 result.push(action);
56 } else {
57 const command = client.protocol2CodeConverter.asCommand(item);
58 result.push(command);
59 }
60 }
61 return result;
62 },
63 (_error) => undefined
64 );
34 } 65 }
66
35 } as any 67 } as any
36 }; 68 };
37 69
38 const res = new lc.LanguageClient( 70 const client = new lc.LanguageClient(
39 'rust-analyzer', 71 'rust-analyzer',
40 'Rust Analyzer Language Server', 72 'Rust Analyzer Language Server',
41 serverOptions, 73 serverOptions,
42 clientOptions, 74 clientOptions,
43 ); 75 );
44 76
45 // To turn on all proposed features use: res.registerProposedFeatures(); 77 // To turn on all proposed features use: client.registerProposedFeatures();
46 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature 78 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature
47 // since they are available on stable. 79 // since they are available on stable.
48 // Note that while these features are stable in vscode their LSP protocol 80 // Note that while these features are stable in vscode their LSP protocol
49 // implementations are still in the "proposed" category for 3.16. 81 // implementations are still in the "proposed" category for 3.16.
50 res.registerFeature(new CallHierarchyFeature(res)); 82 client.registerFeature(new CallHierarchyFeature(client));
51 res.registerFeature(new SemanticTokensFeature(res)); 83 client.registerFeature(new SemanticTokensFeature(client));
84 client.registerFeature(new SnippetTextEditFeature());
85
86 return client;
87}
52 88
53 return res; 89class SnippetTextEditFeature implements lc.StaticFeature {
90 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
91 const caps: any = capabilities.experimental ?? {};
92 caps.snippetTextEdit = true;
93 capabilities.experimental = caps;
94 }
95 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
96 }
97}
98
99function isSnippetEdit(action: lc.CodeAction): boolean {
100 const documentChanges = action.edit?.documentChanges ?? [];
101 for (const edit of documentChanges) {
102 if (lc.TextDocumentEdit.is(edit)) {
103 if (edit.edits.some((indel) => (indel as any).insertTextFormat === lc.InsertTextFormat.Snippet)) {
104 return true;
105 }
106 }
107 }
108 return false;
54} 109}
diff --git a/editors/code/src/color_theme.ts b/editors/code/src/color_theme.ts
deleted file mode 100644
index 5b9327b28..000000000
--- a/editors/code/src/color_theme.ts
+++ /dev/null
@@ -1,129 +0,0 @@
1import * as fs from 'fs';
2import * as jsonc from 'jsonc-parser';
3import * as path from 'path';
4import * as vscode from 'vscode';
5
6export interface TextMateRuleSettings {
7 foreground?: string;
8 background?: string;
9 fontStyle?: string;
10}
11
12export class ColorTheme {
13 private rules: Map<string, TextMateRuleSettings> = new Map();
14
15 static load(): ColorTheme {
16 // Find out current color theme
17 const themeName = vscode.workspace
18 .getConfiguration('workbench')
19 .get('colorTheme');
20
21 if (typeof themeName !== 'string') {
22 // console.warn('workbench.colorTheme is', themeName)
23 return new ColorTheme();
24 }
25 return loadThemeNamed(themeName);
26 }
27
28 static fromRules(rules: TextMateRule[]): ColorTheme {
29 const res = new ColorTheme();
30 for (const rule of rules) {
31 const scopes = typeof rule.scope === 'undefined'
32 ? []
33 : typeof rule.scope === 'string'
34 ? [rule.scope]
35 : rule.scope;
36
37 for (const scope of scopes) {
38 res.rules.set(scope, rule.settings);
39 }
40 }
41 return res;
42 }
43
44 lookup(scopes: string[]): TextMateRuleSettings {
45 let res: TextMateRuleSettings = {};
46 for (const scope of scopes) {
47 this.rules.forEach((value, key) => {
48 if (scope.startsWith(key)) {
49 res = mergeRuleSettings(res, value);
50 }
51 });
52 }
53 return res;
54 }
55
56 mergeFrom(other: ColorTheme) {
57 other.rules.forEach((value, key) => {
58 const merged = mergeRuleSettings(this.rules.get(key), value);
59 this.rules.set(key, merged);
60 });
61 }
62}
63
64function loadThemeNamed(themeName: string): ColorTheme {
65 function isTheme(extension: vscode.Extension<unknown>): boolean {
66 return (
67 extension.extensionKind === vscode.ExtensionKind.UI &&
68 extension.packageJSON.contributes &&
69 extension.packageJSON.contributes.themes
70 );
71 }
72
73 const themePaths: string[] = vscode.extensions.all
74 .filter(isTheme)
75 .flatMap(
76 ext => ext.packageJSON.contributes.themes
77 .filter((it: any) => (it.id || it.label) === themeName)
78 .map((it: any) => path.join(ext.extensionPath, it.path))
79 );
80
81 const res = new ColorTheme();
82 for (const themePath of themePaths) {
83 res.mergeFrom(loadThemeFile(themePath));
84 }
85
86 const globalCustomizations: any = vscode.workspace.getConfiguration('editor').get('tokenColorCustomizations');
87 res.mergeFrom(ColorTheme.fromRules(globalCustomizations?.textMateRules ?? []));
88
89 const themeCustomizations: any = vscode.workspace.getConfiguration('editor.tokenColorCustomizations').get(`[${themeName}]`);
90 res.mergeFrom(ColorTheme.fromRules(themeCustomizations?.textMateRules ?? []));
91
92
93 return res;
94}
95
96function loadThemeFile(themePath: string): ColorTheme {
97 let text;
98 try {
99 text = fs.readFileSync(themePath, 'utf8');
100 } catch {
101 return new ColorTheme();
102 }
103 const obj = jsonc.parse(text);
104 const tokenColors: TextMateRule[] = obj?.tokenColors ?? [];
105 const res = ColorTheme.fromRules(tokenColors);
106
107 for (const include of obj?.include ?? []) {
108 const includePath = path.join(path.dirname(themePath), include);
109 res.mergeFrom(loadThemeFile(includePath));
110 }
111
112 return res;
113}
114
115interface TextMateRule {
116 scope: string | string[];
117 settings: TextMateRuleSettings;
118}
119
120function mergeRuleSettings(
121 defaultSetting: TextMateRuleSettings | undefined,
122 override: TextMateRuleSettings,
123): TextMateRuleSettings {
124 return {
125 foreground: override.foreground ?? defaultSetting?.foreground,
126 background: override.background ?? defaultSetting?.background,
127 fontStyle: override.fontStyle ?? defaultSetting?.fontStyle,
128 };
129}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index bdb7fc3b0..e5ed77e32 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -4,6 +4,7 @@ import * as ra from '../rust-analyzer-api';
4 4
5import { Ctx, Cmd } from '../ctx'; 5import { Ctx, Cmd } from '../ctx';
6import * as sourceChange from '../source_change'; 6import * as sourceChange from '../source_change';
7import { assert } from '../util';
7 8
8export * from './analyzer_status'; 9export * from './analyzer_status';
9export * from './matching_brace'; 10export * from './matching_brace';
@@ -51,3 +52,58 @@ export function selectAndApplySourceChange(ctx: Ctx): Cmd {
51 } 52 }
52 }; 53 };
53} 54}
55
56export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
57 return async (edit: vscode.WorkspaceEdit) => {
58 await applySnippetWorkspaceEdit(edit);
59 };
60}
61
62export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
63 assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
64 const [uri, edits] = edit.entries()[0];
65
66 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
67 if (!editor) return;
68
69 let selection: vscode.Selection | undefined = undefined;
70 let lineDelta = 0;
71 await editor.edit((builder) => {
72 for (const indel of edits) {
73 const parsed = parseSnippet(indel.newText);
74 if (parsed) {
75 const [newText, [placeholderStart, placeholderLength]] = parsed;
76 const prefix = newText.substr(0, placeholderStart);
77 const lastNewline = prefix.lastIndexOf('\n');
78
79 const startLine = indel.range.start.line + lineDelta + countLines(prefix);
80 const startColumn = lastNewline === -1 ?
81 indel.range.start.character + placeholderStart
82 : prefix.length - lastNewline - 1;
83 const endColumn = startColumn + placeholderLength;
84 selection = new vscode.Selection(
85 new vscode.Position(startLine, startColumn),
86 new vscode.Position(startLine, endColumn),
87 );
88 builder.replace(indel.range, newText);
89 } else {
90 lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
91 builder.replace(indel.range, indel.newText);
92 }
93 }
94 });
95 if (selection) editor.selection = selection;
96}
97
98function parseSnippet(snip: string): [string, [number, number]] | undefined {
99 const m = snip.match(/\$(0|\{0:([^}]*)\})/);
100 if (!m) return undefined;
101 const placeholder = m[2] ?? "";
102 const range: [number, number] = [m.index!!, placeholder.length];
103 const insert = snip.replace(m[0], placeholder);
104 return [insert, range];
105}
106
107function countLines(text: string): number {
108 return (text.match(/\n/g) || []).length;
109}
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
index de0614653..0bf1ee6e6 100644
--- a/editors/code/src/commands/join_lines.ts
+++ b/editors/code/src/commands/join_lines.ts
@@ -1,7 +1,7 @@
1import * as ra from '../rust-analyzer-api'; 1import * as ra from '../rust-analyzer-api';
2import * as lc from 'vscode-languageclient';
2 3
3import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd } from '../ctx';
4import { applySourceChange } from '../source_change';
5 5
6export function joinLines(ctx: Ctx): Cmd { 6export function joinLines(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
@@ -9,10 +9,14 @@ export function joinLines(ctx: Ctx): Cmd {
9 const client = ctx.client; 9 const client = ctx.client;
10 if (!editor || !client) return; 10 if (!editor || !client) return;
11 11
12 const change = await client.sendRequest(ra.joinLines, { 12 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
13 range: client.code2ProtocolConverter.asRange(editor.selection), 13 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
14 textDocument: { uri: editor.document.uri.toString() }, 14 textDocument: { uri: editor.document.uri.toString() },
15 }); 15 });
16 await applySourceChange(ctx, change); 16 editor.edit((builder) => {
17 client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
18 builder.replace(edit.range, edit.newText);
19 });
20 });
17 }; 21 };
18} 22}
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts
index 285849db7..a7871c31e 100644
--- a/editors/code/src/commands/on_enter.ts
+++ b/editors/code/src/commands/on_enter.ts
@@ -1,8 +1,8 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api'; 2import * as ra from '../rust-analyzer-api';
3 3
4import { applySourceChange } from '../source_change';
5import { Cmd, Ctx } from '../ctx'; 4import { Cmd, Ctx } from '../ctx';
5import { applySnippetWorkspaceEdit } from '.';
6 6
7async function handleKeypress(ctx: Ctx) { 7async function handleKeypress(ctx: Ctx) {
8 const editor = ctx.activeRustEditor; 8 const editor = ctx.activeRustEditor;
@@ -21,7 +21,8 @@ async function handleKeypress(ctx: Ctx) {
21 }); 21 });
22 if (!change) return false; 22 if (!change) return false;
23 23
24 await applySourceChange(ctx, change); 24 const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
25 await applySnippetWorkspaceEdit(workspaceEdit);
25 return true; 26 return true;
26} 27}
27 28
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
index d77e8188c..0bd30fb07 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/commands/runnables.ts
@@ -1,43 +1,93 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api'; 3import * as ra from '../rust-analyzer-api';
4import * as os from "os";
5 4
6import { Ctx, Cmd } from '../ctx'; 5import { Ctx, Cmd } from '../ctx';
7import { Cargo } from '../cargo'; 6import { startDebugSession, getDebugConfiguration } from '../debug';
8 7
9export function run(ctx: Ctx): Cmd { 8const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
10 let prevRunnable: RunnableQuickPick | undefined;
11 9
12 return async () => { 10async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
13 const editor = ctx.activeRustEditor; 11 const editor = ctx.activeRustEditor;
14 const client = ctx.client; 12 const client = ctx.client;
15 if (!editor || !client) return; 13 if (!editor || !client) return;
16 14
17 const textDocument: lc.TextDocumentIdentifier = { 15 const textDocument: lc.TextDocumentIdentifier = {
18 uri: editor.document.uri.toString(), 16 uri: editor.document.uri.toString(),
19 }; 17 };
20 18
21 const runnables = await client.sendRequest(ra.runnables, { 19 const runnables = await client.sendRequest(ra.runnables, {
22 textDocument, 20 textDocument,
23 position: client.code2ProtocolConverter.asPosition( 21 position: client.code2ProtocolConverter.asPosition(
24 editor.selection.active, 22 editor.selection.active,
25 ), 23 ),
26 }); 24 });
27 const items: RunnableQuickPick[] = []; 25 const items: RunnableQuickPick[] = [];
28 if (prevRunnable) { 26 if (prevRunnable) {
29 items.push(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;
30 } 35 }
31 for (const r of runnables) { 36
32 if ( 37 if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) {
33 prevRunnable && 38 continue;
34 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
35 ) {
36 continue;
37 }
38 items.push(new RunnableQuickPick(r));
39 } 39 }
40 const item = await vscode.window.showQuickPick(items); 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);
41 if (!item) return; 91 if (!item) return;
42 92
43 item.detail = 'rerun'; 93 item.detail = 'rerun';
@@ -64,71 +114,54 @@ export function runSingle(ctx: Ctx): Cmd {
64 }; 114 };
65} 115}
66 116
67function getLldbDebugConfig(config: ra.Runnable, sourceFileMap: Record<string, string>): vscode.DebugConfiguration { 117export function debug(ctx: Ctx): Cmd {
68 return { 118 let prevDebuggee: RunnableQuickPick | undefined;
69 type: "lldb",
70 request: "launch",
71 name: config.label,
72 cargo: {
73 args: config.args,
74 },
75 args: config.extraArgs,
76 cwd: config.cwd,
77 sourceMap: sourceFileMap
78 };
79}
80
81const debugOutput = vscode.window.createOutputChannel("Debug");
82 119
83async function getCppvsDebugConfig(config: ra.Runnable, sourceFileMap: Record<string, string>): Promise<vscode.DebugConfiguration> { 120 return async () => {
84 debugOutput.clear(); 121 const item = await selectRunnable(ctx, prevDebuggee, true);
85 122 if (!item) return;
86 const cargo = new Cargo(config.cwd || '.', debugOutput);
87 const executable = await cargo.executableFromArgs(config.args);
88 123
89 // if we are here, there were no compilation errors. 124 item.detail = 'restart';
90 return { 125 prevDebuggee = item;
91 type: (os.platform() === "win32") ? "cppvsdbg" : 'cppdbg', 126 return await startDebugSession(ctx, item.runnable);
92 request: "launch",
93 name: config.label,
94 program: executable,
95 args: config.extraArgs,
96 cwd: config.cwd,
97 sourceFileMap: sourceFileMap,
98 }; 127 };
99} 128}
100 129
101export function debugSingle(ctx: Ctx): Cmd { 130export function debugSingle(ctx: Ctx): Cmd {
102 return async (config: ra.Runnable) => { 131 return async (config: ra.Runnable) => {
103 const editor = ctx.activeRustEditor; 132 await startDebugSession(ctx, config);
104 if (!editor) return; 133 };
134}
105 135
106 const lldbId = "vadimcn.vscode-lldb"; 136async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise<void> {
107 const cpptoolsId = "ms-vscode.cpptools"; 137 const scope = ctx.activeRustEditor?.document.uri;
138 if (!scope) return;
108 139
109 const debugEngineId = ctx.config.debug.engine; 140 const debugConfig = await getDebugConfiguration(ctx, item.runnable);
110 let debugEngine = null; 141 if (!debugConfig) return;
111 if (debugEngineId === "auto") {
112 debugEngine = vscode.extensions.getExtension(lldbId);
113 if (!debugEngine) {
114 debugEngine = vscode.extensions.getExtension(cpptoolsId);
115 }
116 }
117 else {
118 debugEngine = vscode.extensions.getExtension(debugEngineId);
119 }
120 142
121 if (!debugEngine) { 143 const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
122 vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=${lldbId})` 144 const configurations = wsLaunchSection.get<any[]>("configurations") || [];
123 + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=${cpptoolsId}) extension for debugging.`); 145
124 return; 146 const index = configurations.findIndex(c => c.name === debugConfig.name);
125 } 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}
126 158
127 const debugConfig = lldbId === debugEngine.id 159export function newDebugConfig(ctx: Ctx): Cmd {
128 ? getLldbDebugConfig(config, ctx.config.debug.sourceFileMap) 160 return async () => {
129 : await getCppvsDebugConfig(config, ctx.config.debug.sourceFileMap); 161 const item = await selectRunnable(ctx, undefined, true, false);
162 if (!item) return;
130 163
131 return vscode.debug.startDebugging(undefined, debugConfig); 164 await makeDebugConfig(ctx, item);
132 }; 165 };
133} 166}
134 167
diff --git a/editors/code/src/commands/ssr.ts b/editors/code/src/commands/ssr.ts
index 6fee051fd..4ef8cdf04 100644
--- a/editors/code/src/commands/ssr.ts
+++ b/editors/code/src/commands/ssr.ts
@@ -11,7 +11,7 @@ export function ssr(ctx: Ctx): Cmd {
11 11
12 const options: vscode.InputBoxOptions = { 12 const options: vscode.InputBoxOptions = {
13 value: "() ==>> ()", 13 value: "() ==>> ()",
14 prompt: "EnteR request, for example 'Foo($a:expr) ==> Foo::new($a)' ", 14 prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
15 validateInput: async (x: string) => { 15 validateInput: async (x: string) => {
16 try { 16 try {
17 await client.sendRequest(ra.ssr, { query: x, parseOnly: true }); 17 await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
index cfcf47b2f..a5446c327 100644
--- a/editors/code/src/commands/syntax_tree.ts
+++ b/editors/code/src/commands/syntax_tree.ts
@@ -206,7 +206,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
206 } 206 }
207 207
208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range { 208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
209 const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine); 209 const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
210 if (!parsedRange) return; 210 if (!parsedRange) return;
211 211
212 const [begin, end] = parsedRange 212 const [begin, end] = parsedRange
@@ -225,7 +225,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
225 return doc.positionAt(targetOffset); 225 return doc.positionAt(targetOffset);
226 } 226 }
227 227
228 // Shitty workaround for crlf line endings 228 // Dirty workaround for crlf line endings
229 // We are still in this prehistoric era of carriage returns here... 229 // We are still in this prehistoric era of carriage returns here...
230 230
231 let line = 0; 231 let line = 0;
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 110e54180..ee294fbe3 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -16,6 +16,10 @@ export class Config {
16 "files", 16 "files",
17 "highlighting", 17 "highlighting",
18 "updates.channel", 18 "updates.channel",
19 "lens.enable",
20 "lens.run",
21 "lens.debug",
22 "lens.implementations",
19 ] 23 ]
20 .map(opt => `${this.rootSection}.${opt}`); 24 .map(opt => `${this.rootSection}.${opt}`);
21 25
@@ -94,6 +98,7 @@ export class Config {
94 98
95 get inlayHints() { 99 get inlayHints() {
96 return { 100 return {
101 enable: this.get<boolean>("inlayHints.enable"),
97 typeHints: this.get<boolean>("inlayHints.typeHints"), 102 typeHints: this.get<boolean>("inlayHints.typeHints"),
98 parameterHints: this.get<boolean>("inlayHints.parameterHints"), 103 parameterHints: this.get<boolean>("inlayHints.parameterHints"),
99 chainingHints: this.get<boolean>("inlayHints.chainingHints"), 104 chainingHints: this.get<boolean>("inlayHints.chainingHints"),
@@ -108,10 +113,23 @@ export class Config {
108 } 113 }
109 114
110 get debug() { 115 get debug() {
116 // "/rustc/<id>" used by suggestions only.
117 const { ["/rustc/<id>"]: _, ...sourceFileMap } = this.get<Record<string, string>>("debug.sourceFileMap");
118
111 return { 119 return {
112 engine: this.get<string>("debug.engine"), 120 engine: this.get<string>("debug.engine"),
113 sourceFileMap: this.get<Record<string, string>>("debug.sourceFileMap"), 121 engineSettings: this.get<object>("debug.engineSettings"),
122 openUpDebugPane: this.get<boolean>("debug.openUpDebugPane"),
123 sourceFileMap: sourceFileMap
114 }; 124 };
115 } 125 }
116 126
127 get lens() {
128 return {
129 enable: this.get<boolean>("lens.enable"),
130 run: this.get<boolean>("lens.run"),
131 debug: this.get<boolean>("lens.debug"),
132 implementations: this.get<boolean>("lens.implementations"),
133 };
134 }
117} 135}
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts
new file mode 100644
index 000000000..d3fe588e8
--- /dev/null
+++ b/editors/code/src/debug.ts
@@ -0,0 +1,124 @@
1import * as os from "os";
2import * as vscode from 'vscode';
3import * as path from 'path';
4import * as ra from './rust-analyzer-api';
5
6import { Cargo } from './cargo';
7import { Ctx } from "./ctx";
8
9const debugOutput = vscode.window.createOutputChannel("Debug");
10type DebugConfigProvider = (config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>) => vscode.DebugConfiguration;
11
12function getLldbDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration {
13 return {
14 type: "lldb",
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
25function getCppvsDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration {
26 return {
27 type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg",
28 request: "launch",
29 name: config.label,
30 program: executable,
31 args: config.extraArgs,
32 cwd: config.cwd,
33 sourceFileMap: sourceFileMap,
34 };
35}
36
37async function getDebugExecutable(config: ra.Runnable): Promise<string> {
38 const cargo = new Cargo(config.cwd || '.', debugOutput);
39 const executable = await cargo.executableFromArgs(config.args);
40
41 // if we are here, there were no compilation errors.
42 return executable;
43}
44
45export async function getDebugConfiguration(ctx: Ctx, config: ra.Runnable): Promise<vscode.DebugConfiguration | undefined> {
46 const editor = ctx.activeRustEditor;
47 if (!editor) return;
48
49 const knownEngines: Record<string, DebugConfigProvider> = {
50 "vadimcn.vscode-lldb": getLldbDebugConfig,
51 "ms-vscode.cpptools": getCppvsDebugConfig
52 };
53 const debugOptions = ctx.config.debug;
54
55 let debugEngine = null;
56 if (debugOptions.engine === "auto") {
57 for (var engineId in knownEngines) {
58 debugEngine = vscode.extensions.getExtension(engineId);
59 if (debugEngine) break;
60 }
61 } else {
62 debugEngine = vscode.extensions.getExtension(debugOptions.engine);
63 }
64
65 if (!debugEngine) {
66 vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)`
67 + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`);
68 return;
69 }
70
71 debugOutput.clear();
72 if (ctx.config.debug.openUpDebugPane) {
73 debugOutput.show(true);
74 }
75
76 const wsFolder = path.normalize(vscode.workspace.workspaceFolders![0].uri.fsPath); // folder exists or RA is not active.
77 function simplifyPath(p: string): string {
78 return path.normalize(p).replace(wsFolder, '${workspaceRoot}');
79 }
80
81 const executable = await getDebugExecutable(config);
82 const debugConfig = knownEngines[debugEngine.id](config, simplifyPath(executable), debugOptions.sourceFileMap);
83 if (debugConfig.type in debugOptions.engineSettings) {
84 const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type];
85 for (var key in settingsMap) {
86 debugConfig[key] = settingsMap[key];
87 }
88 }
89
90 if (debugConfig.name === "run binary") {
91 // The LSP side: crates\rust-analyzer\src\main_loop\handlers.rs,
92 // fn to_lsp_runnable(...) with RunnableKind::Bin
93 debugConfig.name = `run ${path.basename(executable)}`;
94 }
95
96 if (debugConfig.cwd) {
97 debugConfig.cwd = simplifyPath(debugConfig.cwd);
98 }
99
100 return debugConfig;
101}
102
103export async function startDebugSession(ctx: Ctx, config: ra.Runnable): Promise<boolean> {
104 let debugConfig: vscode.DebugConfiguration | undefined = undefined;
105 let message = "";
106
107 const wsLaunchSection = vscode.workspace.getConfiguration("launch");
108 const configurations = wsLaunchSection.get<any[]>("configurations") || [];
109
110 const index = configurations.findIndex(c => c.name === config.label);
111 if (-1 !== index) {
112 debugConfig = configurations[index];
113 message = " (from launch.json)";
114 debugOutput.clear();
115 } else {
116 debugConfig = await getDebugConfiguration(ctx, config);
117 }
118
119 if (!debugConfig) return false;
120
121 debugOutput.appendLine(`Launching debug configuration${message}:`);
122 debugOutput.appendLine(JSON.stringify(debugConfig, null, 2));
123 return vscode.debug.startDebugging(undefined, debugConfig);
124}
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index a09531797..a2b07d003 100644
--- a/editors/code/src/inlay_hints.ts
+++ b/editors/code/src/inlay_hints.ts
@@ -10,13 +10,13 @@ export function activateInlayHints(ctx: Ctx) {
10 const maybeUpdater = { 10 const maybeUpdater = {
11 updater: null as null | HintsUpdater, 11 updater: null as null | HintsUpdater,
12 async onConfigChange() { 12 async onConfigChange() {
13 if ( 13 const anyEnabled = ctx.config.inlayHints.typeHints
14 !ctx.config.inlayHints.typeHints && 14 || ctx.config.inlayHints.parameterHints
15 !ctx.config.inlayHints.parameterHints && 15 || ctx.config.inlayHints.chainingHints;
16 !ctx.config.inlayHints.chainingHints 16 const enabled = ctx.config.inlayHints.enable && anyEnabled;
17 ) { 17
18 return this.dispose(); 18 if (!enabled) return this.dispose();
19 } 19
20 await sleep(100); 20 await sleep(100);
21 if (this.updater) { 21 if (this.updater) {
22 this.updater.syncCacheAndRenderHints(); 22 this.updater.syncCacheAndRenderHints();
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index efd56a84b..8b0a9d870 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -8,10 +8,9 @@ import { activateInlayHints } from './inlay_hints';
8import { activateStatusDisplay } from './status_display'; 8import { activateStatusDisplay } from './status_display';
9import { Ctx } from './ctx'; 9import { Ctx } from './ctx';
10import { Config, NIGHTLY_TAG } from './config'; 10import { Config, NIGHTLY_TAG } from './config';
11import { log, assert } from './util'; 11import { log, assert, isValidExecutable } from './util';
12import { PersistentState } from './persistent_state'; 12import { PersistentState } from './persistent_state';
13import { fetchRelease, download } from './net'; 13import { fetchRelease, download } from './net';
14import { spawnSync } from 'child_process';
15import { activateTaskProvider } from './tasks'; 14import { activateTaskProvider } from './tasks';
16 15
17let ctx: Ctx | undefined; 16let ctx: Ctx | undefined;
@@ -78,6 +77,8 @@ export async function activate(context: vscode.ExtensionContext) {
78 ctx.registerCommand('syntaxTree', commands.syntaxTree); 77 ctx.registerCommand('syntaxTree', commands.syntaxTree);
79 ctx.registerCommand('expandMacro', commands.expandMacro); 78 ctx.registerCommand('expandMacro', commands.expandMacro);
80 ctx.registerCommand('run', commands.run); 79 ctx.registerCommand('run', commands.run);
80 ctx.registerCommand('debug', commands.debug);
81 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
81 82
82 defaultOnEnter.dispose(); 83 defaultOnEnter.dispose();
83 ctx.registerCommand('onEnter', commands.onEnter); 84 ctx.registerCommand('onEnter', commands.onEnter);
@@ -90,6 +91,7 @@ export async function activate(context: vscode.ExtensionContext) {
90 ctx.registerCommand('debugSingle', commands.debugSingle); 91 ctx.registerCommand('debugSingle', commands.debugSingle);
91 ctx.registerCommand('showReferences', commands.showReferences); 92 ctx.registerCommand('showReferences', commands.showReferences);
92 ctx.registerCommand('applySourceChange', commands.applySourceChange); 93 ctx.registerCommand('applySourceChange', commands.applySourceChange);
94 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
93 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 95 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
94 96
95 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 97 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
@@ -179,10 +181,7 @@ async function bootstrapServer(config: Config, state: PersistentState): Promise<
179 181
180 log.debug("Using server binary at", path); 182 log.debug("Using server binary at", path);
181 183
182 const res = spawnSync(path, ["--version"], { encoding: 'utf8' }); 184 if (!isValidExecutable(path)) {
183 log.debug("Checked binary availability via --version", res);
184 log.debug(res, "--version output:", res.output);
185 if (res.status !== 0) {
186 throw new Error(`Failed to execute ${path} --version`); 185 throw new Error(`Failed to execute ${path} --version`);
187 } 186 }
188 187
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 400ac3714..8ed56c173 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -64,12 +64,12 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
64 64
65export interface JoinLinesParams { 65export interface JoinLinesParams {
66 textDocument: lc.TextDocumentIdentifier; 66 textDocument: lc.TextDocumentIdentifier;
67 range: lc.Range; 67 ranges: lc.Range[];
68} 68}
69export const joinLines = request<JoinLinesParams, SourceChange>("joinLines"); 69export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
70 70
71 71
72export const onEnter = request<lc.TextDocumentPositionParams, Option<SourceChange>>("onEnter"); 72export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
73 73
74export interface RunnablesParams { 74export interface RunnablesParams {
75 textDocument: lc.TextDocumentIdentifier; 75 textDocument: lc.TextDocumentIdentifier;
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 6f91f81d6..127a9e911 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -1,6 +1,7 @@
1import * as lc from "vscode-languageclient"; 1import * as lc from "vscode-languageclient";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import { strict as nativeAssert } from "assert"; 3import { strict as nativeAssert } from "assert";
4import { spawnSync } from "child_process";
4 5
5export function assert(condition: boolean, explanation: string): asserts condition { 6export function assert(condition: boolean, explanation: string): asserts condition {
6 try { 7 try {
@@ -82,3 +83,13 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
82export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 83export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
83 return isRustDocument(editor.document); 84 return isRustDocument(editor.document);
84} 85}
86
87export function isValidExecutable(path: string): boolean {
88 log.debug("Checking availability of a binary at", path);
89
90 const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
91
92 log.debug(res, "--version output:", res.output);
93
94 return res.status === 0;
95}