aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/ast_inspector.ts (renamed from editors/code/src/commands/syntax_tree.ts)92
-rw-r--r--editors/code/src/cargo.ts106
-rw-r--r--editors/code/src/client.ts98
-rw-r--r--editors/code/src/color_theme.ts129
-rw-r--r--editors/code/src/commands.ts416
-rw-r--r--editors/code/src/commands/analyzer_status.ts51
-rw-r--r--editors/code/src/commands/expand_macro.ts66
-rw-r--r--editors/code/src/commands/index.ts53
-rw-r--r--editors/code/src/commands/join_lines.ts18
-rw-r--r--editors/code/src/commands/matching_brace.ts27
-rw-r--r--editors/code/src/commands/on_enter.ts34
-rw-r--r--editors/code/src/commands/parent_module.ts29
-rw-r--r--editors/code/src/commands/runnables.ts185
-rw-r--r--editors/code/src/commands/server_version.ts15
-rw-r--r--editors/code/src/commands/ssr.ts32
-rw-r--r--editors/code/src/config.ts22
-rw-r--r--editors/code/src/debug.ts147
-rw-r--r--editors/code/src/inlay_hints.ts16
-rw-r--r--editors/code/src/lsp_ext.ts86
-rw-r--r--editors/code/src/main.ts70
-rw-r--r--editors/code/src/run.ts146
-rw-r--r--editors/code/src/rust-analyzer-api.ts125
-rw-r--r--editors/code/src/snippets.ts55
-rw-r--r--editors/code/src/source_change.ts54
-rw-r--r--editors/code/src/tasks.ts7
-rw-r--r--editors/code/src/toolchain.ts174
-rw-r--r--editors/code/src/util.ts43
27 files changed, 1257 insertions, 1039 deletions
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/ast_inspector.ts
index cfcf47b2f..4fdd167bd 100644
--- a/editors/code/src/commands/syntax_tree.ts
+++ b/editors/code/src/ast_inspector.ts
@@ -1,93 +1,15 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd, Disposable } from '../ctx';
5import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7const AST_FILE_SCHEME = "rust-analyzer";
8
9// Opens the virtual file that will show the syntax tree
10//
11// The contents of the file come from the `TextDocumentContentProvider`
12export function syntaxTree(ctx: Ctx): Cmd {
13 const tdcp = new TextDocumentContentProvider(ctx);
14
15 void new AstInspector(ctx);
16
17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
18 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
19 brackets: [["[", ")"]],
20 }));
21
22 return async () => {
23 const editor = vscode.window.activeTextEditor;
24 const rangeEnabled = !!editor && !editor.selection.isEmpty;
25
26 const uri = rangeEnabled
27 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
28 : tdcp.uri;
29
30 const document = await vscode.workspace.openTextDocument(uri);
31
32 tdcp.eventEmitter.fire(uri);
33
34 void await vscode.window.showTextDocument(document, {
35 viewColumn: vscode.ViewColumn.Two,
36 preserveFocus: true
37 });
38 };
39}
40
41class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
42 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
43 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
44
45
46 constructor(private readonly ctx: Ctx) {
47 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
48 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
49 }
50
51 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
52 if (isRustDocument(event.document)) {
53 // We need to order this after language server updates, but there's no API for that.
54 // Hence, good old sleep().
55 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
56 }
57 }
58 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
59 if (editor && isRustEditor(editor)) {
60 this.eventEmitter.fire(this.uri);
61 }
62 }
63
64 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
65 const rustEditor = this.ctx.activeRustEditor;
66 if (!rustEditor) return '';
67
68 // When the range based query is enabled we take the range of the selection
69 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
70 ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
71 : null;
72
73 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
74 return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
75 }
76
77 get onDidChange(): vscode.Event<vscode.Uri> {
78 return this.eventEmitter.event;
79 }
80}
81 2
3import { Ctx, Disposable } from './ctx';
4import { RustEditor, isRustEditor } from './util';
82 5
83// FIXME: consider implementing this via the Tree View API? 6// FIXME: consider implementing this via the Tree View API?
84// https://code.visualstudio.com/api/extension-guides/tree-view 7// https://code.visualstudio.com/api/extension-guides/tree-view
85class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable { 8export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
86 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({ 9 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'), 10 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
88 borderStyle: "solid", 11 borderStyle: "solid",
89 borderWidth: "2px", 12 borderWidth: "2px",
90
91 }); 13 });
92 private rustEditor: undefined | RustEditor; 14 private rustEditor: undefined | RustEditor;
93 15
@@ -113,7 +35,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
113 }); 35 });
114 36
115 constructor(ctx: Ctx) { 37 constructor(ctx: Ctx) {
116 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); 38 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: 'rust-analyzer' }, this));
117 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this)); 39 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
118 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions); 40 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
119 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); 41 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
@@ -146,7 +68,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
146 } 68 }
147 69
148 private findAstTextEditor(): undefined | vscode.TextEditor { 70 private findAstTextEditor(): undefined | vscode.TextEditor {
149 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME); 71 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === 'rust-analyzer');
150 } 72 }
151 73
152 private setRustEditor(newRustEditor: undefined | RustEditor) { 74 private setRustEditor(newRustEditor: undefined | RustEditor) {
@@ -206,7 +128,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
206 } 128 }
207 129
208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range { 130 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
209 const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine); 131 const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
210 if (!parsedRange) return; 132 if (!parsedRange) return;
211 133
212 const [begin, end] = parsedRange 134 const [begin, end] = parsedRange
@@ -225,7 +147,7 @@ class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, D
225 return doc.positionAt(targetOffset); 147 return doc.positionAt(targetOffset);
226 } 148 }
227 149
228 // Shitty workaround for crlf line endings 150 // Dirty workaround for crlf line endings
229 // We are still in this prehistoric era of carriage returns here... 151 // We are still in this prehistoric era of carriage returns here...
230 152
231 let line = 0; 153 let line = 0;
diff --git a/editors/code/src/cargo.ts b/editors/code/src/cargo.ts
deleted file mode 100644
index a328ba9bd..000000000
--- a/editors/code/src/cargo.ts
+++ /dev/null
@@ -1,106 +0,0 @@
1import * as cp from 'child_process';
2import * as readline from 'readline';
3import { OutputChannel } from 'vscode';
4
5interface CompilationArtifact {
6 fileName: string;
7 name: string;
8 kind: string;
9 isTest: boolean;
10}
11
12export class Cargo {
13 rootFolder: string;
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
23 public async artifactsFromArgs(cargoArgs: string[]): Promise<CompilationArtifact[]> {
24 const artifacts: CompilationArtifact[] = [];
25
26 try {
27 await this.runCargo(cargoArgs,
28 message => {
29 if (message.reason === 'compiler-artifact' && message.executable) {
30 const isBinary = message.target.crate_types.includes('bin');
31 const isBuildScript = message.target.kind.includes('custom-build');
32 if ((isBinary && !isBuildScript) || message.profile.test) {
33 artifacts.push({
34 fileName: message.executable,
35 name: message.target.name,
36 kind: message.target.kind[0],
37 isTest: message.profile.test
38 });
39 }
40 }
41 else if (message.reason === 'compiler-message') {
42 this.output.append(message.message.rendered);
43 }
44 },
45 stderr => {
46 this.output.append(stderr);
47 }
48 );
49 }
50 catch (err) {
51 this.output.show(true);
52 throw new Error(`Cargo invocation has failed: ${err}`);
53 }
54
55 return artifacts;
56 }
57
58 public async executableFromArgs(args: string[]): Promise<string> {
59 const cargoArgs = [...args]; // to remain args unchanged
60 cargoArgs.push("--message-format=json");
61
62 const artifacts = await this.artifactsFromArgs(cargoArgs);
63
64 if (artifacts.length === 0) {
65 throw new Error('No compilation artifacts');
66 } else if (artifacts.length > 1) {
67 throw new Error('Multiple compilation artifacts are not supported.');
68 }
69
70 return artifacts[0].fileName;
71 }
72
73 runCargo(
74 cargoArgs: string[],
75 onStdoutJson: (obj: any) => void,
76 onStderrString: (data: string) => void
77 ): Promise<number> {
78 return new Promise<number>((resolve, reject) => {
79 const cargo = cp.spawn('cargo', cargoArgs, {
80 stdio: ['ignore', 'pipe', 'pipe'],
81 cwd: this.rootFolder,
82 env: this.env,
83 });
84
85 cargo.on('error', err => {
86 reject(new Error(`could not launch cargo: ${err}`));
87 });
88 cargo.stderr.on('data', chunk => {
89 onStderrString(chunk.toString());
90 });
91
92 const rl = readline.createInterface({ input: cargo.stdout });
93 rl.on('line', line => {
94 const message = JSON.parse(line);
95 onStdoutJson(message);
96 });
97
98 cargo.on('exit', (exitCode, _) => {
99 if (exitCode === 0)
100 resolve(exitCode);
101 else
102 reject(new Error(`exit code: ${exitCode}.`));
103 });
104 });
105 }
106} \ No newline at end of file
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index cffdcf11a..d64f9a3f9 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -31,24 +31,112 @@ 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 const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>();
45 for (const item of values) {
46 if (lc.CodeAction.is(item)) {
47 const action = client.protocol2CodeConverter.asCodeAction(item);
48 const group = actionGroup(item);
49 if (isSnippetEdit(item) || group) {
50 action.command = {
51 command: "rust-analyzer.applySnippetWorkspaceEdit",
52 title: "",
53 arguments: [action.edit],
54 };
55 action.edit = undefined;
56 }
57
58 if (group) {
59 let entry = groups.get(group);
60 if (!entry) {
61 entry = { index: result.length, items: [] };
62 groups.set(group, entry);
63 result.push(action);
64 }
65 entry.items.push(action);
66 } else {
67 result.push(action);
68 }
69 } else {
70 const command = client.protocol2CodeConverter.asCommand(item);
71 result.push(command);
72 }
73 }
74 for (const [group, { index, items }] of groups) {
75 if (items.length === 1) {
76 result[index] = items[0];
77 } else {
78 const action = new vscode.CodeAction(group);
79 action.command = {
80 command: "rust-analyzer.applyActionGroup",
81 title: "",
82 arguments: [items.map((item) => {
83 return { label: item.title, edit: item.command!!.arguments!![0] };
84 })],
85 };
86 result[index] = action;
87 }
88 }
89 return result;
90 },
91 (_error) => undefined
92 );
34 } 93 }
94
35 } as any 95 } as any
36 }; 96 };
37 97
38 const res = new lc.LanguageClient( 98 const client = new lc.LanguageClient(
39 'rust-analyzer', 99 'rust-analyzer',
40 'Rust Analyzer Language Server', 100 'Rust Analyzer Language Server',
41 serverOptions, 101 serverOptions,
42 clientOptions, 102 clientOptions,
43 ); 103 );
44 104
45 // To turn on all proposed features use: res.registerProposedFeatures(); 105 // To turn on all proposed features use: client.registerProposedFeatures();
46 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature 106 // Here we want to enable CallHierarchyFeature and SemanticTokensFeature
47 // since they are available on stable. 107 // since they are available on stable.
48 // Note that while these features are stable in vscode their LSP protocol 108 // Note that while these features are stable in vscode their LSP protocol
49 // implementations are still in the "proposed" category for 3.16. 109 // implementations are still in the "proposed" category for 3.16.
50 res.registerFeature(new CallHierarchyFeature(res)); 110 client.registerFeature(new CallHierarchyFeature(client));
51 res.registerFeature(new SemanticTokensFeature(res)); 111 client.registerFeature(new SemanticTokensFeature(client));
112 client.registerFeature(new ExperimentalFeatures());
113
114 return client;
115}
116
117class ExperimentalFeatures implements lc.StaticFeature {
118 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
119 const caps: any = capabilities.experimental ?? {};
120 caps.snippetTextEdit = true;
121 caps.codeActionGroup = true;
122 capabilities.experimental = caps;
123 }
124 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
125 }
126}
127
128function isSnippetEdit(action: lc.CodeAction): boolean {
129 const documentChanges = action.edit?.documentChanges ?? [];
130 for (const edit of documentChanges) {
131 if (lc.TextDocumentEdit.is(edit)) {
132 if (edit.edits.some((indel) => (indel as any).insertTextFormat === lc.InsertTextFormat.Snippet)) {
133 return true;
134 }
135 }
136 }
137 return false;
138}
52 139
53 return res; 140function actionGroup(action: lc.CodeAction): string | undefined {
141 return (action as any).group;
54} 142}
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.ts b/editors/code/src/commands.ts
new file mode 100644
index 000000000..534d2a984
--- /dev/null
+++ b/editors/code/src/commands.ts
@@ -0,0 +1,416 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from './lsp_ext';
4
5import { Ctx, Cmd } from './ctx';
6import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
7import { spawnSync } from 'child_process';
8import { RunnableQuickPick, selectRunnable, createTask } from './run';
9import { AstInspector } from './ast_inspector';
10import { isRustDocument, sleep, isRustEditor } from './util';
11import { startDebugSession, makeDebugConfig } from './debug';
12
13export * from './ast_inspector';
14export * from './run';
15
16export function analyzerStatus(ctx: Ctx): Cmd {
17 const tdcp = new class implements vscode.TextDocumentContentProvider {
18 readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
19 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
20
21 provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
22 if (!vscode.window.activeTextEditor) return '';
23
24 return ctx.client.sendRequest(ra.analyzerStatus, null);
25 }
26
27 get onDidChange(): vscode.Event<vscode.Uri> {
28 return this.eventEmitter.event;
29 }
30 }();
31
32 let poller: NodeJS.Timer | undefined = undefined;
33
34 ctx.pushCleanup(
35 vscode.workspace.registerTextDocumentContentProvider(
36 'rust-analyzer-status',
37 tdcp,
38 ),
39 );
40
41 ctx.pushCleanup({
42 dispose() {
43 if (poller !== undefined) {
44 clearInterval(poller);
45 }
46 },
47 });
48
49 return async () => {
50 if (poller === undefined) {
51 poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
52 }
53 const document = await vscode.workspace.openTextDocument(tdcp.uri);
54 return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
55 };
56}
57
58export function matchingBrace(ctx: Ctx): Cmd {
59 return async () => {
60 const editor = ctx.activeRustEditor;
61 const client = ctx.client;
62 if (!editor || !client) return;
63
64 const response = await client.sendRequest(ra.matchingBrace, {
65 textDocument: { uri: editor.document.uri.toString() },
66 positions: editor.selections.map(s =>
67 client.code2ProtocolConverter.asPosition(s.active),
68 ),
69 });
70 editor.selections = editor.selections.map((sel, idx) => {
71 const active = client.protocol2CodeConverter.asPosition(
72 response[idx],
73 );
74 const anchor = sel.isEmpty ? active : sel.anchor;
75 return new vscode.Selection(anchor, active);
76 });
77 editor.revealRange(editor.selection);
78 };
79}
80
81export function joinLines(ctx: Ctx): Cmd {
82 return async () => {
83 const editor = ctx.activeRustEditor;
84 const client = ctx.client;
85 if (!editor || !client) return;
86
87 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
88 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
89 textDocument: { uri: editor.document.uri.toString() },
90 });
91 editor.edit((builder) => {
92 client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
93 builder.replace(edit.range, edit.newText);
94 });
95 });
96 };
97}
98
99export function onEnter(ctx: Ctx): Cmd {
100 async function handleKeypress() {
101 const editor = ctx.activeRustEditor;
102 const client = ctx.client;
103
104 if (!editor || !client) return false;
105
106 const lcEdits = await client.sendRequest(ra.onEnter, {
107 textDocument: { uri: editor.document.uri.toString() },
108 position: client.code2ProtocolConverter.asPosition(
109 editor.selection.active,
110 ),
111 }).catch(_error => {
112 // client.logFailedRequest(OnEnterRequest.type, error);
113 return null;
114 });
115 if (!lcEdits) return false;
116
117 const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
118 await applySnippetTextEdits(editor, edits);
119 return true;
120 }
121
122 return async () => {
123 if (await handleKeypress()) return;
124
125 await vscode.commands.executeCommand('default:type', { text: '\n' });
126 };
127}
128
129export function parentModule(ctx: Ctx): Cmd {
130 return async () => {
131 const editor = ctx.activeRustEditor;
132 const client = ctx.client;
133 if (!editor || !client) return;
134
135 const response = await client.sendRequest(ra.parentModule, {
136 textDocument: { uri: editor.document.uri.toString() },
137 position: client.code2ProtocolConverter.asPosition(
138 editor.selection.active,
139 ),
140 });
141 const loc = response[0];
142 if (!loc) return;
143
144 const uri = client.protocol2CodeConverter.asUri(loc.targetUri);
145 const range = client.protocol2CodeConverter.asRange(loc.targetRange);
146
147 const doc = await vscode.workspace.openTextDocument(uri);
148 const e = await vscode.window.showTextDocument(doc);
149 e.selection = new vscode.Selection(range.start, range.start);
150 e.revealRange(range, vscode.TextEditorRevealType.InCenter);
151 };
152}
153
154export function ssr(ctx: Ctx): Cmd {
155 return async () => {
156 const client = ctx.client;
157 if (!client) return;
158
159 const options: vscode.InputBoxOptions = {
160 value: "() ==>> ()",
161 prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
162 validateInput: async (x: string) => {
163 try {
164 await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
165 } catch (e) {
166 return e.toString();
167 }
168 return null;
169 }
170 };
171 const request = await vscode.window.showInputBox(options);
172 if (!request) return;
173
174 const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
175
176 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
177 };
178}
179
180export function serverVersion(ctx: Ctx): Cmd {
181 return async () => {
182 const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
183 const commitHash = stdout.slice(`rust-analyzer `.length).trim();
184 const { releaseTag } = ctx.config.package;
185
186 void vscode.window.showInformationMessage(
187 `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
188 );
189 };
190}
191
192export function toggleInlayHints(ctx: Ctx): Cmd {
193 return async () => {
194 await vscode
195 .workspace
196 .getConfiguration(`${ctx.config.rootSection}.inlayHints`)
197 .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace);
198 };
199}
200
201// Opens the virtual file that will show the syntax tree
202//
203// The contents of the file come from the `TextDocumentContentProvider`
204export function syntaxTree(ctx: Ctx): Cmd {
205 const tdcp = new class implements vscode.TextDocumentContentProvider {
206 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
207 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
208 constructor() {
209 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
210 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
211 }
212
213 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
214 if (isRustDocument(event.document)) {
215 // We need to order this after language server updates, but there's no API for that.
216 // Hence, good old sleep().
217 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
218 }
219 }
220 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
221 if (editor && isRustEditor(editor)) {
222 this.eventEmitter.fire(this.uri);
223 }
224 }
225
226 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
227 const rustEditor = ctx.activeRustEditor;
228 if (!rustEditor) return '';
229
230 // When the range based query is enabled we take the range of the selection
231 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
232 ? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
233 : null;
234
235 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
236 return ctx.client.sendRequest(ra.syntaxTree, params, ct);
237 }
238
239 get onDidChange(): vscode.Event<vscode.Uri> {
240 return this.eventEmitter.event;
241 }
242 };
243
244 void new AstInspector(ctx);
245
246 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider('rust-analyzer', tdcp));
247 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
248 brackets: [["[", ")"]],
249 }));
250
251 return async () => {
252 const editor = vscode.window.activeTextEditor;
253 const rangeEnabled = !!editor && !editor.selection.isEmpty;
254
255 const uri = rangeEnabled
256 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
257 : tdcp.uri;
258
259 const document = await vscode.workspace.openTextDocument(uri);
260
261 tdcp.eventEmitter.fire(uri);
262
263 void await vscode.window.showTextDocument(document, {
264 viewColumn: vscode.ViewColumn.Two,
265 preserveFocus: true
266 });
267 };
268}
269
270
271// Opens the virtual file that will show the syntax tree
272//
273// The contents of the file come from the `TextDocumentContentProvider`
274export function expandMacro(ctx: Ctx): Cmd {
275 function codeFormat(expanded: ra.ExpandedMacro): string {
276 let result = `// Recursive expansion of ${expanded.name}! macro\n`;
277 result += '// ' + '='.repeat(result.length - 3);
278 result += '\n\n';
279 result += expanded.expansion;
280
281 return result;
282 }
283
284 const tdcp = new class implements vscode.TextDocumentContentProvider {
285 uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
286 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
287 async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
288 const editor = vscode.window.activeTextEditor;
289 const client = ctx.client;
290 if (!editor || !client) return '';
291
292 const position = editor.selection.active;
293
294 const expanded = await client.sendRequest(ra.expandMacro, {
295 textDocument: { uri: editor.document.uri.toString() },
296 position,
297 });
298
299 if (expanded == null) return 'Not available';
300
301 return codeFormat(expanded);
302 }
303
304 get onDidChange(): vscode.Event<vscode.Uri> {
305 return this.eventEmitter.event;
306 }
307 }();
308
309 ctx.pushCleanup(
310 vscode.workspace.registerTextDocumentContentProvider(
311 'rust-analyzer',
312 tdcp,
313 ),
314 );
315
316 return async () => {
317 const document = await vscode.workspace.openTextDocument(tdcp.uri);
318 tdcp.eventEmitter.fire(tdcp.uri);
319 return vscode.window.showTextDocument(
320 document,
321 vscode.ViewColumn.Two,
322 true,
323 );
324 };
325}
326
327export function collectGarbage(ctx: Ctx): Cmd {
328 return async () => ctx.client.sendRequest(ra.collectGarbage, null);
329}
330
331export function showReferences(ctx: Ctx): Cmd {
332 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
333 const client = ctx.client;
334 if (client) {
335 vscode.commands.executeCommand(
336 'editor.action.showReferences',
337 vscode.Uri.parse(uri),
338 client.protocol2CodeConverter.asPosition(position),
339 locations.map(client.protocol2CodeConverter.asLocation),
340 );
341 }
342 };
343}
344
345export function applyActionGroup(_ctx: Ctx): Cmd {
346 return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
347 const selectedAction = await vscode.window.showQuickPick(actions);
348 if (!selectedAction) return;
349 await applySnippetWorkspaceEdit(selectedAction.edit);
350 };
351}
352
353export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
354 return async (edit: vscode.WorkspaceEdit) => {
355 await applySnippetWorkspaceEdit(edit);
356 };
357}
358
359export function run(ctx: Ctx): Cmd {
360 let prevRunnable: RunnableQuickPick | undefined;
361
362 return async () => {
363 const item = await selectRunnable(ctx, prevRunnable);
364 if (!item) return;
365
366 item.detail = 'rerun';
367 prevRunnable = item;
368 const task = createTask(item.runnable);
369 return await vscode.tasks.executeTask(task);
370 };
371}
372
373export function runSingle(ctx: Ctx): Cmd {
374 return async (runnable: ra.Runnable) => {
375 const editor = ctx.activeRustEditor;
376 if (!editor) return;
377
378 const task = createTask(runnable);
379 task.group = vscode.TaskGroup.Build;
380 task.presentationOptions = {
381 reveal: vscode.TaskRevealKind.Always,
382 panel: vscode.TaskPanelKind.Dedicated,
383 clear: true,
384 };
385
386 return vscode.tasks.executeTask(task);
387 };
388}
389
390export function debug(ctx: Ctx): Cmd {
391 let prevDebuggee: RunnableQuickPick | undefined;
392
393 return async () => {
394 const item = await selectRunnable(ctx, prevDebuggee, true);
395 if (!item) return;
396
397 item.detail = 'restart';
398 prevDebuggee = item;
399 return await startDebugSession(ctx, item.runnable);
400 };
401}
402
403export function debugSingle(ctx: Ctx): Cmd {
404 return async (config: ra.Runnable) => {
405 await startDebugSession(ctx, config);
406 };
407}
408
409export function newDebugConfig(ctx: Ctx): Cmd {
410 return async () => {
411 const item = await selectRunnable(ctx, undefined, true, false);
412 if (!item) return;
413
414 await makeDebugConfig(ctx, item.runnable);
415 };
416}
diff --git a/editors/code/src/commands/analyzer_status.ts b/editors/code/src/commands/analyzer_status.ts
deleted file mode 100644
index 09daa3402..000000000
--- a/editors/code/src/commands/analyzer_status.ts
+++ /dev/null
@@ -1,51 +0,0 @@
1import * as vscode from 'vscode';
2
3import * as ra from '../rust-analyzer-api';
4import { Ctx, Cmd } from '../ctx';
5
6// Shows status of rust-analyzer (for debugging)
7export function analyzerStatus(ctx: Ctx): Cmd {
8 let poller: NodeJS.Timer | undefined = undefined;
9 const tdcp = new TextDocumentContentProvider(ctx);
10
11 ctx.pushCleanup(
12 vscode.workspace.registerTextDocumentContentProvider(
13 'rust-analyzer-status',
14 tdcp,
15 ),
16 );
17
18 ctx.pushCleanup({
19 dispose() {
20 if (poller !== undefined) {
21 clearInterval(poller);
22 }
23 },
24 });
25
26 return async () => {
27 if (poller === undefined) {
28 poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
29 }
30 const document = await vscode.workspace.openTextDocument(tdcp.uri);
31 return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
32 };
33}
34
35class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
36 readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
37 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
38
39 constructor(private readonly ctx: Ctx) {
40 }
41
42 provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
43 if (!vscode.window.activeTextEditor) return '';
44
45 return this.ctx.client.sendRequest(ra.analyzerStatus, null);
46 }
47
48 get onDidChange(): vscode.Event<vscode.Uri> {
49 return this.eventEmitter.event;
50 }
51}
diff --git a/editors/code/src/commands/expand_macro.ts b/editors/code/src/commands/expand_macro.ts
deleted file mode 100644
index 23f2ef1d5..000000000
--- a/editors/code/src/commands/expand_macro.ts
+++ /dev/null
@@ -1,66 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd } from '../ctx';
5
6// Opens the virtual file that will show the syntax tree
7//
8// The contents of the file come from the `TextDocumentContentProvider`
9export function expandMacro(ctx: Ctx): Cmd {
10 const tdcp = new TextDocumentContentProvider(ctx);
11 ctx.pushCleanup(
12 vscode.workspace.registerTextDocumentContentProvider(
13 'rust-analyzer',
14 tdcp,
15 ),
16 );
17
18 return async () => {
19 const document = await vscode.workspace.openTextDocument(tdcp.uri);
20 tdcp.eventEmitter.fire(tdcp.uri);
21 return vscode.window.showTextDocument(
22 document,
23 vscode.ViewColumn.Two,
24 true,
25 );
26 };
27}
28
29function codeFormat(expanded: ra.ExpandedMacro): string {
30 let result = `// Recursive expansion of ${expanded.name}! macro\n`;
31 result += '// ' + '='.repeat(result.length - 3);
32 result += '\n\n';
33 result += expanded.expansion;
34
35 return result;
36}
37
38class TextDocumentContentProvider
39 implements vscode.TextDocumentContentProvider {
40 uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
41 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
42
43 constructor(private readonly ctx: Ctx) {
44 }
45
46 async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
47 const editor = vscode.window.activeTextEditor;
48 const client = this.ctx.client;
49 if (!editor || !client) return '';
50
51 const position = editor.selection.active;
52
53 const expanded = await client.sendRequest(ra.expandMacro, {
54 textDocument: { uri: editor.document.uri.toString() },
55 position,
56 });
57
58 if (expanded == null) return 'Not available';
59
60 return codeFormat(expanded);
61 }
62
63 get onDidChange(): vscode.Event<vscode.Uri> {
64 return this.eventEmitter.event;
65 }
66}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
deleted file mode 100644
index bdb7fc3b0..000000000
--- a/editors/code/src/commands/index.ts
+++ /dev/null
@@ -1,53 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api';
4
5import { Ctx, Cmd } from '../ctx';
6import * as sourceChange from '../source_change';
7
8export * from './analyzer_status';
9export * from './matching_brace';
10export * from './join_lines';
11export * from './on_enter';
12export * from './parent_module';
13export * from './syntax_tree';
14export * from './expand_macro';
15export * from './runnables';
16export * from './ssr';
17export * from './server_version';
18
19export function collectGarbage(ctx: Ctx): Cmd {
20 return async () => ctx.client.sendRequest(ra.collectGarbage, null);
21}
22
23export function showReferences(ctx: Ctx): Cmd {
24 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
25 const client = ctx.client;
26 if (client) {
27 vscode.commands.executeCommand(
28 'editor.action.showReferences',
29 vscode.Uri.parse(uri),
30 client.protocol2CodeConverter.asPosition(position),
31 locations.map(client.protocol2CodeConverter.asLocation),
32 );
33 }
34 };
35}
36
37export function applySourceChange(ctx: Ctx): Cmd {
38 return async (change: ra.SourceChange) => {
39 await sourceChange.applySourceChange(ctx, change);
40 };
41}
42
43export function selectAndApplySourceChange(ctx: Ctx): Cmd {
44 return async (changes: ra.SourceChange[]) => {
45 if (changes.length === 1) {
46 await sourceChange.applySourceChange(ctx, changes[0]);
47 } else if (changes.length > 0) {
48 const selectedChange = await vscode.window.showQuickPick(changes);
49 if (!selectedChange) return;
50 await sourceChange.applySourceChange(ctx, selectedChange);
51 }
52 };
53}
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
deleted file mode 100644
index de0614653..000000000
--- a/editors/code/src/commands/join_lines.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import * as ra from '../rust-analyzer-api';
2
3import { Ctx, Cmd } from '../ctx';
4import { applySourceChange } from '../source_change';
5
6export function joinLines(ctx: Ctx): Cmd {
7 return async () => {
8 const editor = ctx.activeRustEditor;
9 const client = ctx.client;
10 if (!editor || !client) return;
11
12 const change = await client.sendRequest(ra.joinLines, {
13 range: client.code2ProtocolConverter.asRange(editor.selection),
14 textDocument: { uri: editor.document.uri.toString() },
15 });
16 await applySourceChange(ctx, change);
17 };
18}
diff --git a/editors/code/src/commands/matching_brace.ts b/editors/code/src/commands/matching_brace.ts
deleted file mode 100644
index a60776e2d..000000000
--- a/editors/code/src/commands/matching_brace.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd } from '../ctx';
5
6export function matchingBrace(ctx: Ctx): Cmd {
7 return async () => {
8 const editor = ctx.activeRustEditor;
9 const client = ctx.client;
10 if (!editor || !client) return;
11
12 const response = await client.sendRequest(ra.findMatchingBrace, {
13 textDocument: { uri: editor.document.uri.toString() },
14 offsets: editor.selections.map(s =>
15 client.code2ProtocolConverter.asPosition(s.active),
16 ),
17 });
18 editor.selections = editor.selections.map((sel, idx) => {
19 const active = client.protocol2CodeConverter.asPosition(
20 response[idx],
21 );
22 const anchor = sel.isEmpty ? active : sel.anchor;
23 return new vscode.Selection(anchor, active);
24 });
25 editor.revealRange(editor.selection);
26 };
27}
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts
deleted file mode 100644
index 285849db7..000000000
--- a/editors/code/src/commands/on_enter.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { applySourceChange } from '../source_change';
5import { Cmd, Ctx } from '../ctx';
6
7async function handleKeypress(ctx: Ctx) {
8 const editor = ctx.activeRustEditor;
9 const client = ctx.client;
10
11 if (!editor || !client) return false;
12
13 const change = await client.sendRequest(ra.onEnter, {
14 textDocument: { uri: editor.document.uri.toString() },
15 position: client.code2ProtocolConverter.asPosition(
16 editor.selection.active,
17 ),
18 }).catch(_error => {
19 // client.logFailedRequest(OnEnterRequest.type, error);
20 return null;
21 });
22 if (!change) return false;
23
24 await applySourceChange(ctx, change);
25 return true;
26}
27
28export function onEnter(ctx: Ctx): Cmd {
29 return async () => {
30 if (await handleKeypress(ctx)) return;
31
32 await vscode.commands.executeCommand('default:type', { text: '\n' });
33 };
34}
diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts
deleted file mode 100644
index 8f78ddd71..000000000
--- a/editors/code/src/commands/parent_module.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd } from '../ctx';
5
6export function parentModule(ctx: Ctx): Cmd {
7 return async () => {
8 const editor = ctx.activeRustEditor;
9 const client = ctx.client;
10 if (!editor || !client) return;
11
12 const response = await client.sendRequest(ra.parentModule, {
13 textDocument: { uri: editor.document.uri.toString() },
14 position: client.code2ProtocolConverter.asPosition(
15 editor.selection.active,
16 ),
17 });
18 const loc = response[0];
19 if (loc == null) return;
20
21 const uri = client.protocol2CodeConverter.asUri(loc.uri);
22 const range = client.protocol2CodeConverter.asRange(loc.range);
23
24 const doc = await vscode.workspace.openTextDocument(uri);
25 const e = await vscode.window.showTextDocument(doc);
26 e.selection = new vscode.Selection(range.start, range.start);
27 e.revealRange(range, vscode.TextEditorRevealType.InCenter);
28 };
29}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
deleted file mode 100644
index d77e8188c..000000000
--- a/editors/code/src/commands/runnables.ts
+++ /dev/null
@@ -1,185 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api';
4import * as os from "os";
5
6import { Ctx, Cmd } from '../ctx';
7import { Cargo } from '../cargo';
8
9export function run(ctx: Ctx): Cmd {
10 let prevRunnable: RunnableQuickPick | undefined;
11
12 return async () => {
13 const editor = ctx.activeRustEditor;
14 const client = ctx.client;
15 if (!editor || !client) return;
16
17 const textDocument: lc.TextDocumentIdentifier = {
18 uri: editor.document.uri.toString(),
19 };
20
21 const runnables = await client.sendRequest(ra.runnables, {
22 textDocument,
23 position: client.code2ProtocolConverter.asPosition(
24 editor.selection.active,
25 ),
26 });
27 const items: RunnableQuickPick[] = [];
28 if (prevRunnable) {
29 items.push(prevRunnable);
30 }
31 for (const r of runnables) {
32 if (
33 prevRunnable &&
34 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
35 ) {
36 continue;
37 }
38 items.push(new RunnableQuickPick(r));
39 }
40 const item = await vscode.window.showQuickPick(items);
41 if (!item) return;
42
43 item.detail = 'rerun';
44 prevRunnable = item;
45 const task = createTask(item.runnable);
46 return await vscode.tasks.executeTask(task);
47 };
48}
49
50export function runSingle(ctx: Ctx): Cmd {
51 return async (runnable: ra.Runnable) => {
52 const editor = ctx.activeRustEditor;
53 if (!editor) return;
54
55 const task = createTask(runnable);
56 task.group = vscode.TaskGroup.Build;
57 task.presentationOptions = {
58 reveal: vscode.TaskRevealKind.Always,
59 panel: vscode.TaskPanelKind.Dedicated,
60 clear: true,
61 };
62
63 return vscode.tasks.executeTask(task);
64 };
65}
66
67function getLldbDebugConfig(config: ra.Runnable, sourceFileMap: Record<string, string>): vscode.DebugConfiguration {
68 return {
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
83async function getCppvsDebugConfig(config: ra.Runnable, sourceFileMap: Record<string, string>): Promise<vscode.DebugConfiguration> {
84 debugOutput.clear();
85
86 const cargo = new Cargo(config.cwd || '.', debugOutput);
87 const executable = await cargo.executableFromArgs(config.args);
88
89 // if we are here, there were no compilation errors.
90 return {
91 type: (os.platform() === "win32") ? "cppvsdbg" : 'cppdbg',
92 request: "launch",
93 name: config.label,
94 program: executable,
95 args: config.extraArgs,
96 cwd: config.cwd,
97 sourceFileMap: sourceFileMap,
98 };
99}
100
101export function debugSingle(ctx: Ctx): Cmd {
102 return async (config: ra.Runnable) => {
103 const editor = ctx.activeRustEditor;
104 if (!editor) return;
105
106 const lldbId = "vadimcn.vscode-lldb";
107 const cpptoolsId = "ms-vscode.cpptools";
108
109 const debugEngineId = ctx.config.debug.engine;
110 let debugEngine = null;
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
121 if (!debugEngine) {
122 vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=${lldbId})`
123 + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=${cpptoolsId}) extension for debugging.`);
124 return;
125 }
126
127 const debugConfig = lldbId === debugEngine.id
128 ? getLldbDebugConfig(config, ctx.config.debug.sourceFileMap)
129 : await getCppvsDebugConfig(config, ctx.config.debug.sourceFileMap);
130
131 return vscode.debug.startDebugging(undefined, debugConfig);
132 };
133}
134
135class RunnableQuickPick implements vscode.QuickPickItem {
136 public label: string;
137 public description?: string | undefined;
138 public detail?: string | undefined;
139 public picked?: boolean | undefined;
140
141 constructor(public runnable: ra.Runnable) {
142 this.label = runnable.label;
143 }
144}
145
146interface CargoTaskDefinition extends vscode.TaskDefinition {
147 type: 'cargo';
148 label: string;
149 command: string;
150 args: string[];
151 env?: { [key: string]: string };
152}
153
154function createTask(spec: ra.Runnable): vscode.Task {
155 const TASK_SOURCE = 'Rust';
156 const definition: CargoTaskDefinition = {
157 type: 'cargo',
158 label: spec.label,
159 command: spec.bin,
160 args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
161 env: spec.env,
162 };
163
164 const execOption: vscode.ShellExecutionOptions = {
165 cwd: spec.cwd || '.',
166 env: definition.env,
167 };
168 const exec = new vscode.ShellExecution(
169 definition.command,
170 definition.args,
171 execOption,
172 );
173
174 const f = vscode.workspace.workspaceFolders![0];
175 const t = new vscode.Task(
176 definition,
177 f,
178 definition.label,
179 TASK_SOURCE,
180 exec,
181 ['$rustc'],
182 );
183 t.presentationOptions.clear = true;
184 return t;
185}
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts
deleted file mode 100644
index d64ac726e..000000000
--- a/editors/code/src/commands/server_version.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as vscode from "vscode";
2import { spawnSync } from "child_process";
3import { Ctx, Cmd } from '../ctx';
4
5export function serverVersion(ctx: Ctx): Cmd {
6 return async () => {
7 const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
8 const commitHash = stdout.slice(`rust-analyzer `.length).trim();
9 const { releaseTag } = ctx.config.package;
10
11 void vscode.window.showInformationMessage(
12 `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
13 );
14 };
15}
diff --git a/editors/code/src/commands/ssr.ts b/editors/code/src/commands/ssr.ts
deleted file mode 100644
index 6fee051fd..000000000
--- a/editors/code/src/commands/ssr.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from "../rust-analyzer-api";
3
4import { Ctx, Cmd } from '../ctx';
5import { applySourceChange } from '../source_change';
6
7export function ssr(ctx: Ctx): Cmd {
8 return async () => {
9 const client = ctx.client;
10 if (!client) return;
11
12 const options: vscode.InputBoxOptions = {
13 value: "() ==>> ()",
14 prompt: "EnteR request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
15 validateInput: async (x: string) => {
16 try {
17 await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
18 } catch (e) {
19 return e.toString();
20 }
21 return null;
22 }
23 };
24 const request = await vscode.window.showInputBox(options);
25
26 if (!request) return;
27
28 const change = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
29
30 await applySourceChange(ctx, change);
31 };
32}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 110e54180..e8abf8284 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -8,7 +8,7 @@ export const NIGHTLY_TAG = "nightly";
8export class Config { 8export class Config {
9 readonly extensionId = "matklad.rust-analyzer"; 9 readonly extensionId = "matklad.rust-analyzer";
10 10
11 private readonly rootSection = "rust-analyzer"; 11 readonly rootSection = "rust-analyzer";
12 private readonly requiresReloadOpts = [ 12 private readonly requiresReloadOpts = [
13 "serverPath", 13 "serverPath",
14 "cargo", 14 "cargo",
@@ -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..a0c9b3ab2
--- /dev/null
+++ b/editors/code/src/debug.ts
@@ -0,0 +1,147 @@
1import * as os from "os";
2import * as vscode from 'vscode';
3import * as path from 'path';
4import * as ra from './lsp_ext';
5
6import { Cargo } from './toolchain';
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
12export async function makeDebugConfig(ctx: Ctx, runnable: ra.Runnable): Promise<void> {
13 const scope = ctx.activeRustEditor?.document.uri;
14 if (!scope) return;
15
16 const debugConfig = await getDebugConfiguration(ctx, runnable);
17 if (!debugConfig) return;
18
19 const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
20 const configurations = wsLaunchSection.get<any[]>("configurations") || [];
21
22 const index = configurations.findIndex(c => c.name === debugConfig.name);
23 if (index !== -1) {
24 const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update');
25 if (answer === "Cancel") return;
26
27 configurations[index] = debugConfig;
28 } else {
29 configurations.push(debugConfig);
30 }
31
32 await wsLaunchSection.update("configurations", configurations);
33}
34
35export async function startDebugSession(ctx: Ctx, runnable: ra.Runnable): Promise<boolean> {
36 let debugConfig: vscode.DebugConfiguration | undefined = undefined;
37 let message = "";
38
39 const wsLaunchSection = vscode.workspace.getConfiguration("launch");
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);
56}
57
58async function getDebugConfiguration(ctx: Ctx, runnable: ra.Runnable): Promise<vscode.DebugConfiguration | undefined> {
59 const editor = ctx.activeRustEditor;
60 if (!editor) return;
61
62 const knownEngines: Record<string, DebugConfigProvider> = {
63 "vadimcn.vscode-lldb": getLldbDebugConfig,
64 "ms-vscode.cpptools": getCppvsDebugConfig
65 };
66 const debugOptions = ctx.config.debug;
67
68 let debugEngine = null;
69 if (debugOptions.engine === "auto") {
70 for (var engineId in knownEngines) {
71 debugEngine = vscode.extensions.getExtension(engineId);
72 if (debugEngine) break;
73 }
74 } else {
75 debugEngine = vscode.extensions.getExtension(debugOptions.engine);
76 }
77
78 if (!debugEngine) {
79 vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)`
80 + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`);
81 return;
82 }
83
84 debugOutput.clear();
85 if (ctx.config.debug.openUpDebugPane) {
86 debugOutput.show(true);
87 }
88
89 const wsFolder = path.normalize(vscode.workspace.workspaceFolders![0].uri.fsPath); // folder exists or RA is not active.
90 function simplifyPath(p: string): string {
91 return path.normalize(p).replace(wsFolder, '${workspaceRoot}');
92 }
93
94 const executable = await getDebugExecutable(runnable);
95 const debugConfig = knownEngines[debugEngine.id](runnable, simplifyPath(executable), debugOptions.sourceFileMap);
96 if (debugConfig.type in debugOptions.engineSettings) {
97 const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type];
98 for (var key in settingsMap) {
99 debugConfig[key] = settingsMap[key];
100 }
101 }
102
103 if (debugConfig.name === "run binary") {
104 // The LSP side: crates\rust-analyzer\src\main_loop\handlers.rs,
105 // fn to_lsp_runnable(...) with RunnableKind::Bin
106 debugConfig.name = `run ${path.basename(executable)}`;
107 }
108
109 if (debugConfig.cwd) {
110 debugConfig.cwd = simplifyPath(debugConfig.cwd);
111 }
112
113 return debugConfig;
114}
115
116async function getDebugExecutable(runnable: ra.Runnable): Promise<string> {
117 const cargo = new Cargo(runnable.args.workspaceRoot || '.', debugOutput);
118 const executable = await cargo.executableFromArgs(runnable.args.cargoArgs);
119
120 // if we are here, there were no compilation errors.
121 return executable;
122}
123
124function 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}
136
137function getCppvsDebugConfig(runnable: ra.Runnable, executable: string, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration {
138 return {
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 };
147}
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index a09531797..9e6d6045f 100644
--- a/editors/code/src/inlay_hints.ts
+++ b/editors/code/src/inlay_hints.ts
@@ -1,6 +1,6 @@
1import * as lc from "vscode-languageclient"; 1import * as lc from "vscode-languageclient";
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import * as ra from './rust-analyzer-api'; 3import * as ra from './lsp_ext';
4 4
5import { Ctx, Disposable } from './ctx'; 5import { Ctx, Disposable } from './ctx';
6import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util'; 6import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util';
@@ -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/lsp_ext.ts b/editors/code/src/lsp_ext.ts
new file mode 100644
index 000000000..c51acfccb
--- /dev/null
+++ b/editors/code/src/lsp_ext.ts
@@ -0,0 +1,86 @@
1/**
2 * This file mirrors `crates/rust-analyzer/src/req.rs` declarations.
3 */
4
5import * as lc from "vscode-languageclient";
6
7export const analyzerStatus = new lc.RequestType<null, string, void>("rust-analyzer/analyzerStatus");
8
9export const collectGarbage = new lc.RequestType<null, null, void>("rust-analyzer/collectGarbage");
10
11export interface SyntaxTreeParams {
12 textDocument: lc.TextDocumentIdentifier;
13 range: lc.Range | null;
14}
15export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>("rust-analyzer/syntaxTree");
16
17
18export interface ExpandMacroParams {
19 textDocument: lc.TextDocumentIdentifier;
20 position: lc.Position;
21}
22export interface ExpandedMacro {
23 name: string;
24 expansion: string;
25}
26export const expandMacro = new lc.RequestType<ExpandMacroParams, ExpandedMacro | null, void>("rust-analyzer/expandMacro");
27
28export interface MatchingBraceParams {
29 textDocument: lc.TextDocumentIdentifier;
30 positions: lc.Position[];
31}
32export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position[], void>("experimental/matchingBrace");
33
34export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule");
35
36export interface JoinLinesParams {
37 textDocument: lc.TextDocumentIdentifier;
38 ranges: lc.Range[];
39}
40export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], void>("experimental/joinLines");
41
42export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], void>("experimental/onEnter");
43
44export interface RunnablesParams {
45 textDocument: lc.TextDocumentIdentifier;
46 position: lc.Position | null;
47}
48
49export interface Runnable {
50 label: string;
51 location?: lc.LocationLink;
52 kind: "cargo";
53 args: {
54 workspaceRoot?: string;
55 cargoArgs: string[];
56 executableArgs: string[];
57 };
58}
59export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables");
60
61export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
62
63export namespace InlayHint {
64 export const enum Kind {
65 TypeHint = "TypeHint",
66 ParamHint = "ParameterHint",
67 ChainingHint = "ChainingHint",
68 }
69 interface Common {
70 range: lc.Range;
71 label: string;
72 }
73 export type TypeHint = Common & { kind: Kind.TypeHint };
74 export type ParamHint = Common & { kind: Kind.ParamHint };
75 export type ChainingHint = Common & { kind: Kind.ChainingHint };
76}
77export interface InlayHintsParams {
78 textDocument: lc.TextDocumentIdentifier;
79}
80export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void>("rust-analyzer/inlayHints");
81
82export interface SsrParams {
83 query: string;
84 parseOnly: boolean;
85}
86export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index efd56a84b..b7337621c 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -1,21 +1,24 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as path from "path"; 2import * as path from "path";
3import * as os from "os"; 3import * as os from "os";
4import { promises as fs } from "fs"; 4import { promises as fs, PathLike } from "fs";
5 5
6import * as commands from './commands'; 6import * as commands from './commands';
7import { activateInlayHints } from './inlay_hints'; 7import { 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';
15import { setContextValue } from './util';
16import { exec } from 'child_process';
16 17
17let ctx: Ctx | undefined; 18let ctx: Ctx | undefined;
18 19
20const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
21
19export async function activate(context: vscode.ExtensionContext) { 22export async function activate(context: vscode.ExtensionContext) {
20 // Register a "dumb" onEnter command for the case where server fails to 23 // Register a "dumb" onEnter command for the case where server fails to
21 // start. 24 // start.
@@ -54,6 +57,8 @@ export async function activate(context: vscode.ExtensionContext) {
54 // This a horribly, horribly wrong way to deal with this problem. 57 // This a horribly, horribly wrong way to deal with this problem.
55 ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath); 58 ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
56 59
60 setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
61
57 // Commands which invokes manually via command palette, shortcut, etc. 62 // Commands which invokes manually via command palette, shortcut, etc.
58 63
59 // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895 64 // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
@@ -78,19 +83,22 @@ export async function activate(context: vscode.ExtensionContext) {
78 ctx.registerCommand('syntaxTree', commands.syntaxTree); 83 ctx.registerCommand('syntaxTree', commands.syntaxTree);
79 ctx.registerCommand('expandMacro', commands.expandMacro); 84 ctx.registerCommand('expandMacro', commands.expandMacro);
80 ctx.registerCommand('run', commands.run); 85 ctx.registerCommand('run', commands.run);
86 ctx.registerCommand('debug', commands.debug);
87 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
81 88
82 defaultOnEnter.dispose(); 89 defaultOnEnter.dispose();
83 ctx.registerCommand('onEnter', commands.onEnter); 90 ctx.registerCommand('onEnter', commands.onEnter);
84 91
85 ctx.registerCommand('ssr', commands.ssr); 92 ctx.registerCommand('ssr', commands.ssr);
86 ctx.registerCommand('serverVersion', commands.serverVersion); 93 ctx.registerCommand('serverVersion', commands.serverVersion);
94 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
87 95
88 // Internal commands which are invoked by the server. 96 // Internal commands which are invoked by the server.
89 ctx.registerCommand('runSingle', commands.runSingle); 97 ctx.registerCommand('runSingle', commands.runSingle);
90 ctx.registerCommand('debugSingle', commands.debugSingle); 98 ctx.registerCommand('debugSingle', commands.debugSingle);
91 ctx.registerCommand('showReferences', commands.showReferences); 99 ctx.registerCommand('showReferences', commands.showReferences);
92 ctx.registerCommand('applySourceChange', commands.applySourceChange); 100 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
93 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 101 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
94 102
95 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 103 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
96 104
@@ -106,6 +114,7 @@ export async function activate(context: vscode.ExtensionContext) {
106} 114}
107 115
108export async function deactivate() { 116export async function deactivate() {
117 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
109 await ctx?.client.stop(); 118 await ctx?.client.stop();
110 ctx = undefined; 119 ctx = undefined;
111} 120}
@@ -179,16 +188,53 @@ async function bootstrapServer(config: Config, state: PersistentState): Promise<
179 188
180 log.debug("Using server binary at", path); 189 log.debug("Using server binary at", path);
181 190
182 const res = spawnSync(path, ["--version"], { encoding: 'utf8' }); 191 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`); 192 throw new Error(`Failed to execute ${path} --version`);
187 } 193 }
188 194
189 return path; 195 return path;
190} 196}
191 197
198async function patchelf(dest: PathLike): Promise<void> {
199 await vscode.window.withProgress(
200 {
201 location: vscode.ProgressLocation.Notification,
202 title: "Patching rust-analyzer for NixOS"
203 },
204 async (progress, _) => {
205 const expression = `
206 {src, pkgs ? import <nixpkgs> {}}:
207 pkgs.stdenv.mkDerivation {
208 name = "rust-analyzer";
209 inherit src;
210 phases = [ "installPhase" "fixupPhase" ];
211 installPhase = "cp $src $out";
212 fixupPhase = ''
213 chmod 755 $out
214 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
215 '';
216 }
217 `;
218 const origFile = dest + "-orig";
219 await fs.rename(dest, origFile);
220 progress.report({ message: "Patching executable", increment: 20 });
221 await new Promise((resolve, reject) => {
222 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
223 (err, stdout, stderr) => {
224 if (err != null) {
225 reject(Error(stderr));
226 } else {
227 resolve(stdout);
228 }
229 });
230 handle.stdin?.write(expression);
231 handle.stdin?.end();
232 });
233 await fs.unlink(origFile);
234 }
235 );
236}
237
192async function getServer(config: Config, state: PersistentState): Promise<string | undefined> { 238async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
193 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; 239 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
194 if (explicitPath) { 240 if (explicitPath) {
@@ -238,6 +284,12 @@ async function getServer(config: Config, state: PersistentState): Promise<string
238 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); 284 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
239 285
240 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 }); 286 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
287
288 // Patching executable if that's NixOS.
289 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
290 await patchelf(dest);
291 }
292
241 await state.updateServerVersion(config.package.version); 293 await state.updateServerVersion(config.package.version);
242 return dest; 294 return dest;
243} 295}
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
new file mode 100644
index 000000000..5c790741f
--- /dev/null
+++ b/editors/code/src/run.ts
@@ -0,0 +1,146 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from './lsp_ext';
4import * as toolchain from "./toolchain";
5
6import { Ctx } from './ctx';
7import { makeDebugConfig } from './debug';
8
9const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
10
11export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
12 const editor = ctx.activeRustEditor;
13 const client = ctx.client;
14 if (!editor || !client) return;
15
16 const textDocument: lc.TextDocumentIdentifier = {
17 uri: editor.document.uri.toString(),
18 };
19
20 const runnables = await client.sendRequest(ra.runnables, {
21 textDocument,
22 position: client.code2ProtocolConverter.asPosition(
23 editor.selection.active,
24 ),
25 });
26 const items: RunnableQuickPick[] = [];
27 if (prevRunnable) {
28 items.push(prevRunnable);
29 }
30 for (const r of runnables) {
31 if (
32 prevRunnable &&
33 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
34 ) {
35 continue;
36 }
37
38 if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) {
39 continue;
40 }
41 items.push(new RunnableQuickPick(r));
42 }
43
44 if (items.length === 0) {
45 // it is the debug case, run always has at least 'cargo check ...'
46 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables
47 vscode.window.showErrorMessage("There's no debug target!");
48 return;
49 }
50
51 return await new Promise((resolve) => {
52 const disposables: vscode.Disposable[] = [];
53 const close = (result?: RunnableQuickPick) => {
54 resolve(result);
55 disposables.forEach(d => d.dispose());
56 };
57
58 const quickPick = vscode.window.createQuickPick<RunnableQuickPick>();
59 quickPick.items = items;
60 quickPick.title = "Select Runnable";
61 if (showButtons) {
62 quickPick.buttons = quickPickButtons;
63 }
64 disposables.push(
65 quickPick.onDidHide(() => close()),
66 quickPick.onDidAccept(() => close(quickPick.selectedItems[0])),
67 quickPick.onDidTriggerButton((_button) => {
68 (async () => await makeDebugConfig(ctx, quickPick.activeItems[0].runnable))();
69 close();
70 }),
71 quickPick.onDidChangeActive((active) => {
72 if (showButtons && active.length > 0) {
73 if (active[0].label.startsWith('cargo')) {
74 // save button makes no sense for `cargo test` or `cargo check`
75 quickPick.buttons = [];
76 } else if (quickPick.buttons.length === 0) {
77 quickPick.buttons = quickPickButtons;
78 }
79 }
80 }),
81 quickPick
82 );
83 quickPick.show();
84 });
85}
86
87export class RunnableQuickPick implements vscode.QuickPickItem {
88 public label: string;
89 public description?: string | undefined;
90 public detail?: string | undefined;
91 public picked?: boolean | undefined;
92
93 constructor(public runnable: ra.Runnable) {
94 this.label = runnable.label;
95 }
96}
97
98interface CargoTaskDefinition extends vscode.TaskDefinition {
99 type: 'cargo';
100 label: string;
101 command: string;
102 args: string[];
103 env?: { [key: string]: string };
104}
105
106export function createTask(runnable: ra.Runnable): vscode.Task {
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;
114 if (runnable.args.executableArgs.length > 0) {
115 args.push('--', ...runnable.args.executableArgs);
116 }
117 const definition: CargoTaskDefinition = {
118 type: 'cargo',
119 label: runnable.label,
120 command,
121 args,
122 env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }),
123 };
124
125 const execOption: vscode.ShellExecutionOptions = {
126 cwd: runnable.args.workspaceRoot || '.',
127 env: definition.env,
128 };
129 const exec = new vscode.ShellExecution(
130 definition.command,
131 definition.args,
132 execOption,
133 );
134
135 const f = vscode.workspace.workspaceFolders![0];
136 const t = new vscode.Task(
137 definition,
138 f,
139 definition.label,
140 TASK_SOURCE,
141 exec,
142 ['$rustc'],
143 );
144 t.presentationOptions.clear = true;
145 return t;
146}
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
deleted file mode 100644
index 400ac3714..000000000
--- a/editors/code/src/rust-analyzer-api.ts
+++ /dev/null
@@ -1,125 +0,0 @@
1/**
2 * This file mirrors `crates/rust-analyzer/src/req.rs` declarations.
3 */
4
5import * as lc from "vscode-languageclient";
6
7type Option<T> = null | T;
8type Vec<T> = T[];
9type FxHashMap<K extends PropertyKey, V> = Record<K, V>;
10
11function request<TParams, TResult>(method: string) {
12 return new lc.RequestType<TParams, TResult, unknown>(`rust-analyzer/${method}`);
13}
14function notification<TParam>(method: string) {
15 return new lc.NotificationType<TParam>(method);
16}
17
18
19export const analyzerStatus = request<null, string>("analyzerStatus");
20
21
22export const collectGarbage = request<null, null>("collectGarbage");
23
24
25export interface SyntaxTreeParams {
26 textDocument: lc.TextDocumentIdentifier;
27 range: Option<lc.Range>;
28}
29export const syntaxTree = request<SyntaxTreeParams, string>("syntaxTree");
30
31
32export interface ExpandMacroParams {
33 textDocument: lc.TextDocumentIdentifier;
34 position: Option<lc.Position>;
35}
36export interface ExpandedMacro {
37 name: string;
38 expansion: string;
39}
40export const expandMacro = request<ExpandMacroParams, Option<ExpandedMacro>>("expandMacro");
41
42
43export interface FindMatchingBraceParams {
44 textDocument: lc.TextDocumentIdentifier;
45 offsets: Vec<lc.Position>;
46}
47export const findMatchingBrace = request<FindMatchingBraceParams, Vec<lc.Position>>("findMatchingBrace");
48
49
50export interface PublishDecorationsParams {
51 uri: string;
52 decorations: Vec<Decoration>;
53}
54export interface Decoration {
55 range: lc.Range;
56 tag: string;
57 bindingHash: Option<string>;
58}
59export const decorationsRequest = request<lc.TextDocumentIdentifier, Vec<Decoration>>("decorationsRequest");
60
61
62export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Location>>("parentModule");
63
64
65export interface JoinLinesParams {
66 textDocument: lc.TextDocumentIdentifier;
67 range: lc.Range;
68}
69export const joinLines = request<JoinLinesParams, SourceChange>("joinLines");
70
71
72export const onEnter = request<lc.TextDocumentPositionParams, Option<SourceChange>>("onEnter");
73
74export interface RunnablesParams {
75 textDocument: lc.TextDocumentIdentifier;
76 position: Option<lc.Position>;
77}
78export interface Runnable {
79 range: lc.Range;
80 label: string;
81 bin: string;
82 args: Vec<string>;
83 extraArgs: Vec<string>;
84 env: FxHashMap<string, string>;
85 cwd: Option<string>;
86}
87export const runnables = request<RunnablesParams, Vec<Runnable>>("runnables");
88
89export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
90
91export namespace InlayHint {
92 export const enum Kind {
93 TypeHint = "TypeHint",
94 ParamHint = "ParameterHint",
95 ChainingHint = "ChainingHint",
96 }
97 interface Common {
98 range: lc.Range;
99 label: string;
100 }
101 export type TypeHint = Common & { kind: Kind.TypeHint };
102 export type ParamHint = Common & { kind: Kind.ParamHint };
103 export type ChainingHint = Common & { kind: Kind.ChainingHint };
104}
105export interface InlayHintsParams {
106 textDocument: lc.TextDocumentIdentifier;
107}
108export const inlayHints = request<InlayHintsParams, Vec<InlayHint>>("inlayHints");
109
110
111export interface SsrParams {
112 query: string;
113 parseOnly: boolean;
114}
115export const ssr = request<SsrParams, SourceChange>("ssr");
116
117
118export const publishDecorations = notification<PublishDecorationsParams>("publishDecorations");
119
120
121export interface SourceChange {
122 label: string;
123 workspaceEdit: lc.WorkspaceEdit;
124 cursorPosition: Option<lc.TextDocumentPositionParams>;
125}
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
new file mode 100644
index 000000000..bcb3f2cc7
--- /dev/null
+++ b/editors/code/src/snippets.ts
@@ -0,0 +1,55 @@
1import * as vscode from 'vscode';
2
3import { assert } from './util';
4
5export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
6 assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
7 const [uri, edits] = edit.entries()[0];
8
9 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
10 if (!editor) return;
11 await applySnippetTextEdits(editor, edits);
12}
13
14export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
15 let selection: vscode.Selection | undefined = undefined;
16 let lineDelta = 0;
17 await editor.edit((builder) => {
18 for (const indel of edits) {
19 const parsed = parseSnippet(indel.newText);
20 if (parsed) {
21 const [newText, [placeholderStart, placeholderLength]] = parsed;
22 const prefix = newText.substr(0, placeholderStart);
23 const lastNewline = prefix.lastIndexOf('\n');
24
25 const startLine = indel.range.start.line + lineDelta + countLines(prefix);
26 const startColumn = lastNewline === -1 ?
27 indel.range.start.character + placeholderStart
28 : prefix.length - lastNewline - 1;
29 const endColumn = startColumn + placeholderLength;
30 selection = new vscode.Selection(
31 new vscode.Position(startLine, startColumn),
32 new vscode.Position(startLine, endColumn),
33 );
34 builder.replace(indel.range, newText);
35 } else {
36 lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
37 builder.replace(indel.range, indel.newText);
38 }
39 }
40 });
41 if (selection) editor.selection = selection;
42}
43
44function parseSnippet(snip: string): [string, [number, number]] | undefined {
45 const m = snip.match(/\$(0|\{0:([^}]*)\})/);
46 if (!m) return undefined;
47 const placeholder = m[2] ?? "";
48 const range: [number, number] = [m.index!!, placeholder.length];
49 const insert = snip.replace(m[0], placeholder);
50 return [insert, range];
51}
52
53function countLines(text: string): number {
54 return (text.match(/\n/g) || []).length;
55}
diff --git a/editors/code/src/source_change.ts b/editors/code/src/source_change.ts
deleted file mode 100644
index af8f1df51..000000000
--- a/editors/code/src/source_change.ts
+++ /dev/null
@@ -1,54 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import * as ra from './rust-analyzer-api';
4
5import { Ctx } from './ctx';
6
7export async function applySourceChange(ctx: Ctx, change: ra.SourceChange) {
8 const client = ctx.client;
9 if (!client) return;
10
11 const wsEdit = client.protocol2CodeConverter.asWorkspaceEdit(
12 change.workspaceEdit,
13 );
14 let created;
15 let moved;
16 if (change.workspaceEdit.documentChanges) {
17 for (const docChange of change.workspaceEdit.documentChanges) {
18 if (lc.CreateFile.is(docChange)) {
19 created = docChange.uri;
20 } else if (lc.RenameFile.is(docChange)) {
21 moved = docChange.newUri;
22 }
23 }
24 }
25 const toOpen = created || moved;
26 const toReveal = change.cursorPosition;
27 await vscode.workspace.applyEdit(wsEdit);
28 if (toOpen) {
29 const toOpenUri = vscode.Uri.parse(toOpen);
30 const doc = await vscode.workspace.openTextDocument(toOpenUri);
31 await vscode.window.showTextDocument(doc);
32 } else if (toReveal) {
33 const uri = client.protocol2CodeConverter.asUri(
34 toReveal.textDocument.uri,
35 );
36 const position = client.protocol2CodeConverter.asPosition(
37 toReveal.position,
38 );
39 const editor = vscode.window.activeTextEditor;
40 if (!editor || !editor.selection.isEmpty) {
41 return;
42 }
43
44 if (editor.document.uri !== uri) {
45 const doc = await vscode.workspace.openTextDocument(uri);
46 await vscode.window.showTextDocument(doc);
47 }
48 editor.selection = new vscode.Selection(position, position);
49 editor.revealRange(
50 new vscode.Range(position, position),
51 vscode.TextEditorRevealType.Default,
52 );
53 }
54}
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 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * 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 {
80export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { 83export 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/toolchain.ts b/editors/code/src/toolchain.ts
new file mode 100644
index 000000000..80a7915e9
--- /dev/null
+++ b/editors/code/src/toolchain.ts
@@ -0,0 +1,174 @@
1import * as cp from 'child_process';
2import * as os from 'os';
3import * as path from 'path';
4import * as fs from 'fs';
5import * as readline from 'readline';
6import { OutputChannel } from 'vscode';
7import { log, memoize } from './util';
8
9interface CompilationArtifact {
10 fileName: string;
11 name: string;
12 kind: string;
13 isTest: boolean;
14}
15
16export interface ArtifactSpec {
17 cargoArgs: string[];
18 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
19}
20
21export class Cargo {
22 constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
23
24 // Made public for testing purposes
25 static artifactSpec(args: readonly string[]): ArtifactSpec {
26 const cargoArgs = [...args, "--message-format=json"];
27
28 // arguments for a runnable from the quick pick should be updated.
29 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
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;
37 }
38 }
39
40 const result: ArtifactSpec = { cargoArgs: cargoArgs };
41 if (cargoArgs[0] === "test") {
42 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
43 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
44 result.filter = (artifacts) => artifacts.filter(it => it.isTest);
45 }
46
47 return result;
48 }
49
50 private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
51 const artifacts: CompilationArtifact[] = [];
52
53 try {
54 await this.runCargo(spec.cargoArgs,
55 message => {
56 if (message.reason === 'compiler-artifact' && message.executable) {
57 const isBinary = message.target.crate_types.includes('bin');
58 const isBuildScript = message.target.kind.includes('custom-build');
59 if ((isBinary && !isBuildScript) || message.profile.test) {
60 artifacts.push({
61 fileName: message.executable,
62 name: message.target.name,
63 kind: message.target.kind[0],
64 isTest: message.profile.test
65 });
66 }
67 } else if (message.reason === 'compiler-message') {
68 this.output.append(message.message.rendered);
69 }
70 },
71 stderr => this.output.append(stderr),
72 );
73 } catch (err) {
74 this.output.show(true);
75 throw new Error(`Cargo invocation has failed: ${err}`);
76 }
77
78 return spec.filter?.(artifacts) ?? artifacts;
79 }
80
81 async executableFromArgs(args: readonly string[]): Promise<string> {
82 const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
83
84 if (artifacts.length === 0) {
85 throw new Error('No compilation artifacts');
86 } else if (artifacts.length > 1) {
87 throw new Error('Multiple compilation artifacts are not supported.');
88 }
89
90 return artifacts[0].fileName;
91 }
92
93 private runCargo(
94 cargoArgs: string[],
95 onStdoutJson: (obj: any) => void,
96 onStderrString: (data: string) => void
97 ): Promise<number> {
98 return new Promise((resolve, reject) => {
99 const cargo = cp.spawn(cargoPath(), cargoArgs, {
100 stdio: ['ignore', 'pipe', 'pipe'],
101 cwd: this.rootFolder
102 });
103
104 cargo.on('error', err => reject(new Error(`could not launch cargo: ${err}`)));
105
106 cargo.stderr.on('data', chunk => onStderrString(chunk.toString()));
107
108 const rl = readline.createInterface({ input: cargo.stdout });
109 rl.on('line', line => {
110 const message = JSON.parse(line);
111 onStdoutJson(message);
112 });
113
114 cargo.on('exit', (exitCode, _) => {
115 if (exitCode === 0)
116 resolve(exitCode);
117 else
118 reject(new Error(`exit code: ${exitCode}.`));
119 });
120 });
121 }
122}
123
124/** Mirrors `ra_toolchain::cargo()` implementation */
125export function cargoPath(): string {
126 return getPathForExecutable("cargo");
127}
128
129/** Mirrors `ra_toolchain::get_path_for_executable()` implementation */
130export 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;
139
140 try {
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);
144
145 if (isFile(standardPath)) return standardPath;
146 } catch (err) {
147 log.error("Failed to read the fs info", err);
148 }
149 return executableName;
150 }
151);
152
153function lookupInPath(exec: string): boolean {
154 const paths = process.env.PATH ?? "";;
155
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 });
162
163 return candidates.some(isFile);
164}
165
166function isFile(suspectPath: string): boolean {
167 // It is not mentionned in docs, but `statSync()` throws an error when
168 // the path doesn't exist
169 try {
170 return fs.statSync(suspectPath).isFile();
171 } catch {
172 return false;
173 }
174}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 6f91f81d6..fe3fb71cd 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 {
@@ -73,12 +74,46 @@ export type RustDocument = vscode.TextDocument & { languageId: "rust" };
73export type RustEditor = vscode.TextEditor & { document: RustDocument }; 74export type RustEditor = vscode.TextEditor & { document: RustDocument };
74 75
75export function isRustDocument(document: vscode.TextDocument): document is RustDocument { 76export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
76 return document.languageId === 'rust' 77 // Prevent corrupted text (particularly via inlay hints) in diff views
77 // SCM diff views have the same URI as the on-disk document but not the same content 78 // by allowing only `file` schemes
78 && document.uri.scheme !== 'git' 79 // unfortunately extensions that use diff views not always set this
79 && document.uri.scheme !== 'svn'; 80 // to something different than 'file' (see ongoing bug: #4608)
81 return document.languageId === 'rust' && document.uri.scheme === 'file';
80} 82}
81 83
82export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 84export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
83 return isRustDocument(editor.document); 85 return isRustDocument(editor.document);
84} 86}
87
88export function isValidExecutable(path: string): boolean {
89 log.debug("Checking availability of a binary at", path);
90
91 const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
92
93 log.debug(res, "--version output:", res.output);
94
95 return res.status === 0;
96}
97
98/** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */
99export function setContextValue(key: string, value: any): Thenable<void> {
100 return vscode.commands.executeCommand('setContext', key, value);
101}
102
103/**
104 * Returns a higher-order function that caches the results of invoking the
105 * underlying function.
106 */
107export 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}