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)88
-rw-r--r--editors/code/src/cargo.ts57
-rw-r--r--editors/code/src/client.ts41
-rw-r--r--editors/code/src/commands.ts370
-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.ts88
-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/server_version.ts15
-rw-r--r--editors/code/src/commands/ssr.ts32
-rw-r--r--editors/code/src/config.ts2
-rw-r--r--editors/code/src/debug.ts2
-rw-r--r--editors/code/src/inlay_hints.ts2
-rw-r--r--editors/code/src/lsp_ext.ts84
-rw-r--r--editors/code/src/main.ts55
-rw-r--r--editors/code/src/run.ts (renamed from editors/code/src/commands/runnables.ts)26
-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
22 files changed, 645 insertions, 676 deletions
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/ast_inspector.ts
index a5446c327..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) {
diff --git a/editors/code/src/cargo.ts b/editors/code/src/cargo.ts
index 6a41873d0..a55b2f860 100644
--- a/editors/code/src/cargo.ts
+++ b/editors/code/src/cargo.ts
@@ -12,14 +12,44 @@ interface CompilationArtifact {
12 isTest: boolean; 12 isTest: boolean;
13} 13}
14 14
15export interface ArtifactSpec {
16 cargoArgs: string[];
17 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
18}
19
20export function artifactSpec(args: readonly string[]): ArtifactSpec {
21 const cargoArgs = [...args, "--message-format=json"];
22
23 // arguments for a runnable from the quick pick should be updated.
24 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
25 switch (cargoArgs[0]) {
26 case "run": cargoArgs[0] = "build"; break;
27 case "test": {
28 if (!cargoArgs.includes("--no-run")) {
29 cargoArgs.push("--no-run");
30 }
31 break;
32 }
33 }
34
35 const result: ArtifactSpec = { cargoArgs: cargoArgs };
36 if (cargoArgs[0] === "test") {
37 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
38 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
39 result.filter = (artifacts) => artifacts.filter(it => it.isTest);
40 }
41
42 return result;
43}
44
15export class Cargo { 45export class Cargo {
16 constructor(readonly rootFolder: string, readonly output: OutputChannel) { } 46 constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
17 47
18 private async artifactsFromArgs(cargoArgs: string[]): Promise<CompilationArtifact[]> { 48 private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
19 const artifacts: CompilationArtifact[] = []; 49 const artifacts: CompilationArtifact[] = [];
20 50
21 try { 51 try {
22 await this.runCargo(cargoArgs, 52 await this.runCargo(spec.cargoArgs,
23 message => { 53 message => {
24 if (message.reason === 'compiler-artifact' && message.executable) { 54 if (message.reason === 'compiler-artifact' && message.executable) {
25 const isBinary = message.target.crate_types.includes('bin'); 55 const isBinary = message.target.crate_types.includes('bin');
@@ -43,30 +73,11 @@ export class Cargo {
43 throw new Error(`Cargo invocation has failed: ${err}`); 73 throw new Error(`Cargo invocation has failed: ${err}`);
44 } 74 }
45 75
46 return artifacts; 76 return spec.filter?.(artifacts) ?? artifacts;
47 } 77 }
48 78
49 async executableFromArgs(args: readonly string[]): Promise<string> { 79 async executableFromArgs(args: readonly string[]): Promise<string> {
50 const cargoArgs = [...args, "--message-format=json"]; 80 const artifacts = await this.getArtifacts(artifactSpec(args));
51
52 // arguments for a runnable from the quick pick should be updated.
53 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
54 switch (cargoArgs[0]) {
55 case "run": cargoArgs[0] = "build"; break;
56 case "test": {
57 if (cargoArgs.indexOf("--no-run") === -1) {
58 cargoArgs.push("--no-run");
59 }
60 break;
61 }
62 }
63
64 let artifacts = await this.artifactsFromArgs(cargoArgs);
65 if (cargoArgs[0] === "test") {
66 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
67 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
68 artifacts = artifacts.filter(a => a.isTest);
69 }
70 81
71 if (artifacts.length === 0) { 82 if (artifacts.length === 0) {
72 throw new Error('No compilation artifacts'); 83 throw new Error('No compilation artifacts');
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index fac1a0be3..d64f9a3f9 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -41,10 +41,12 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => { 41 return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => {
42 if (values === null) return undefined; 42 if (values === null) return undefined;
43 const result: (vscode.CodeAction | vscode.Command)[] = []; 43 const result: (vscode.CodeAction | vscode.Command)[] = [];
44 const groups = new Map<string, { index: number; items: vscode.CodeAction[] }>();
44 for (const item of values) { 45 for (const item of values) {
45 if (lc.CodeAction.is(item)) { 46 if (lc.CodeAction.is(item)) {
46 const action = client.protocol2CodeConverter.asCodeAction(item); 47 const action = client.protocol2CodeConverter.asCodeAction(item);
47 if (isSnippetEdit(item)) { 48 const group = actionGroup(item);
49 if (isSnippetEdit(item) || group) {
48 action.command = { 50 action.command = {
49 command: "rust-analyzer.applySnippetWorkspaceEdit", 51 command: "rust-analyzer.applySnippetWorkspaceEdit",
50 title: "", 52 title: "",
@@ -52,12 +54,38 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
52 }; 54 };
53 action.edit = undefined; 55 action.edit = undefined;
54 } 56 }
55 result.push(action); 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 }
56 } else { 69 } else {
57 const command = client.protocol2CodeConverter.asCommand(item); 70 const command = client.protocol2CodeConverter.asCommand(item);
58 result.push(command); 71 result.push(command);
59 } 72 }
60 } 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 }
61 return result; 89 return result;
62 }, 90 },
63 (_error) => undefined 91 (_error) => undefined
@@ -81,15 +109,16 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
81 // implementations are still in the "proposed" category for 3.16. 109 // implementations are still in the "proposed" category for 3.16.
82 client.registerFeature(new CallHierarchyFeature(client)); 110 client.registerFeature(new CallHierarchyFeature(client));
83 client.registerFeature(new SemanticTokensFeature(client)); 111 client.registerFeature(new SemanticTokensFeature(client));
84 client.registerFeature(new SnippetTextEditFeature()); 112 client.registerFeature(new ExperimentalFeatures());
85 113
86 return client; 114 return client;
87} 115}
88 116
89class SnippetTextEditFeature implements lc.StaticFeature { 117class ExperimentalFeatures implements lc.StaticFeature {
90 fillClientCapabilities(capabilities: lc.ClientCapabilities): void { 118 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
91 const caps: any = capabilities.experimental ?? {}; 119 const caps: any = capabilities.experimental ?? {};
92 caps.snippetTextEdit = true; 120 caps.snippetTextEdit = true;
121 caps.codeActionGroup = true;
93 capabilities.experimental = caps; 122 capabilities.experimental = caps;
94 } 123 }
95 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void { 124 initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
@@ -107,3 +136,7 @@ function isSnippetEdit(action: lc.CodeAction): boolean {
107 } 136 }
108 return false; 137 return false;
109} 138}
139
140function actionGroup(action: lc.CodeAction): string | undefined {
141 return (action as any).group;
142}
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
new file mode 100644
index 000000000..86302db37
--- /dev/null
+++ b/editors/code/src/commands.ts
@@ -0,0 +1,370 @@
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';
11
12export * from './ast_inspector';
13export * from './run';
14
15export function analyzerStatus(ctx: Ctx): Cmd {
16 const tdcp = new class implements vscode.TextDocumentContentProvider {
17 readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
18 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
19
20 provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
21 if (!vscode.window.activeTextEditor) return '';
22
23 return ctx.client.sendRequest(ra.analyzerStatus, null);
24 }
25
26 get onDidChange(): vscode.Event<vscode.Uri> {
27 return this.eventEmitter.event;
28 }
29 }();
30
31 let poller: NodeJS.Timer | undefined = undefined;
32
33 ctx.pushCleanup(
34 vscode.workspace.registerTextDocumentContentProvider(
35 'rust-analyzer-status',
36 tdcp,
37 ),
38 );
39
40 ctx.pushCleanup({
41 dispose() {
42 if (poller !== undefined) {
43 clearInterval(poller);
44 }
45 },
46 });
47
48 return async () => {
49 if (poller === undefined) {
50 poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
51 }
52 const document = await vscode.workspace.openTextDocument(tdcp.uri);
53 return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
54 };
55}
56
57export function matchingBrace(ctx: Ctx): Cmd {
58 return async () => {
59 const editor = ctx.activeRustEditor;
60 const client = ctx.client;
61 if (!editor || !client) return;
62
63 const response = await client.sendRequest(ra.matchingBrace, {
64 textDocument: { uri: editor.document.uri.toString() },
65 positions: editor.selections.map(s =>
66 client.code2ProtocolConverter.asPosition(s.active),
67 ),
68 });
69 editor.selections = editor.selections.map((sel, idx) => {
70 const active = client.protocol2CodeConverter.asPosition(
71 response[idx],
72 );
73 const anchor = sel.isEmpty ? active : sel.anchor;
74 return new vscode.Selection(anchor, active);
75 });
76 editor.revealRange(editor.selection);
77 };
78}
79
80export function joinLines(ctx: Ctx): Cmd {
81 return async () => {
82 const editor = ctx.activeRustEditor;
83 const client = ctx.client;
84 if (!editor || !client) return;
85
86 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
87 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
88 textDocument: { uri: editor.document.uri.toString() },
89 });
90 editor.edit((builder) => {
91 client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
92 builder.replace(edit.range, edit.newText);
93 });
94 });
95 };
96}
97
98export function onEnter(ctx: Ctx): Cmd {
99 async function handleKeypress() {
100 const editor = ctx.activeRustEditor;
101 const client = ctx.client;
102
103 if (!editor || !client) return false;
104
105 const lcEdits = await client.sendRequest(ra.onEnter, {
106 textDocument: { uri: editor.document.uri.toString() },
107 position: client.code2ProtocolConverter.asPosition(
108 editor.selection.active,
109 ),
110 }).catch(_error => {
111 // client.logFailedRequest(OnEnterRequest.type, error);
112 return null;
113 });
114 if (!lcEdits) return false;
115
116 const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
117 await applySnippetTextEdits(editor, edits);
118 return true;
119 }
120
121 return async () => {
122 if (await handleKeypress()) return;
123
124 await vscode.commands.executeCommand('default:type', { text: '\n' });
125 };
126}
127
128export function parentModule(ctx: Ctx): Cmd {
129 return async () => {
130 const editor = ctx.activeRustEditor;
131 const client = ctx.client;
132 if (!editor || !client) return;
133
134 const response = await client.sendRequest(ra.parentModule, {
135 textDocument: { uri: editor.document.uri.toString() },
136 position: client.code2ProtocolConverter.asPosition(
137 editor.selection.active,
138 ),
139 });
140 const loc = response[0];
141 if (!loc) return;
142
143 const uri = client.protocol2CodeConverter.asUri(loc.targetUri);
144 const range = client.protocol2CodeConverter.asRange(loc.targetRange);
145
146 const doc = await vscode.workspace.openTextDocument(uri);
147 const e = await vscode.window.showTextDocument(doc);
148 e.selection = new vscode.Selection(range.start, range.start);
149 e.revealRange(range, vscode.TextEditorRevealType.InCenter);
150 };
151}
152
153export function ssr(ctx: Ctx): Cmd {
154 return async () => {
155 const client = ctx.client;
156 if (!client) return;
157
158 const options: vscode.InputBoxOptions = {
159 value: "() ==>> ()",
160 prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
161 validateInput: async (x: string) => {
162 try {
163 await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
164 } catch (e) {
165 return e.toString();
166 }
167 return null;
168 }
169 };
170 const request = await vscode.window.showInputBox(options);
171 if (!request) return;
172
173 const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
174
175 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
176 };
177}
178
179export function serverVersion(ctx: Ctx): Cmd {
180 return async () => {
181 const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
182 const commitHash = stdout.slice(`rust-analyzer `.length).trim();
183 const { releaseTag } = ctx.config.package;
184
185 void vscode.window.showInformationMessage(
186 `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
187 );
188 };
189}
190
191export function toggleInlayHints(ctx: Ctx): Cmd {
192 return async () => {
193 await vscode
194 .workspace
195 .getConfiguration(`${ctx.config.rootSection}.inlayHints`)
196 .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace);
197 };
198}
199
200export function run(ctx: Ctx): Cmd {
201 let prevRunnable: RunnableQuickPick | undefined;
202
203 return async () => {
204 const item = await selectRunnable(ctx, prevRunnable);
205 if (!item) return;
206
207 item.detail = 'rerun';
208 prevRunnable = item;
209 const task = createTask(item.runnable);
210 return await vscode.tasks.executeTask(task);
211 };
212}
213
214// Opens the virtual file that will show the syntax tree
215//
216// The contents of the file come from the `TextDocumentContentProvider`
217export function syntaxTree(ctx: Ctx): Cmd {
218 const tdcp = new class implements vscode.TextDocumentContentProvider {
219 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
220 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
221 constructor() {
222 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
223 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
224 }
225
226 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
227 if (isRustDocument(event.document)) {
228 // We need to order this after language server updates, but there's no API for that.
229 // Hence, good old sleep().
230 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
231 }
232 }
233 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
234 if (editor && isRustEditor(editor)) {
235 this.eventEmitter.fire(this.uri);
236 }
237 }
238
239 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
240 const rustEditor = ctx.activeRustEditor;
241 if (!rustEditor) return '';
242
243 // When the range based query is enabled we take the range of the selection
244 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
245 ? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
246 : null;
247
248 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
249 return ctx.client.sendRequest(ra.syntaxTree, params, ct);
250 }
251
252 get onDidChange(): vscode.Event<vscode.Uri> {
253 return this.eventEmitter.event;
254 }
255 };
256
257 void new AstInspector(ctx);
258
259 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider('rust-analyzer', tdcp));
260 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
261 brackets: [["[", ")"]],
262 }));
263
264 return async () => {
265 const editor = vscode.window.activeTextEditor;
266 const rangeEnabled = !!editor && !editor.selection.isEmpty;
267
268 const uri = rangeEnabled
269 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
270 : tdcp.uri;
271
272 const document = await vscode.workspace.openTextDocument(uri);
273
274 tdcp.eventEmitter.fire(uri);
275
276 void await vscode.window.showTextDocument(document, {
277 viewColumn: vscode.ViewColumn.Two,
278 preserveFocus: true
279 });
280 };
281}
282
283
284// Opens the virtual file that will show the syntax tree
285//
286// The contents of the file come from the `TextDocumentContentProvider`
287export function expandMacro(ctx: Ctx): Cmd {
288 function codeFormat(expanded: ra.ExpandedMacro): string {
289 let result = `// Recursive expansion of ${expanded.name}! macro\n`;
290 result += '// ' + '='.repeat(result.length - 3);
291 result += '\n\n';
292 result += expanded.expansion;
293
294 return result;
295 }
296
297 const tdcp = new class implements vscode.TextDocumentContentProvider {
298 uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
299 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
300 async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
301 const editor = vscode.window.activeTextEditor;
302 const client = ctx.client;
303 if (!editor || !client) return '';
304
305 const position = editor.selection.active;
306
307 const expanded = await client.sendRequest(ra.expandMacro, {
308 textDocument: { uri: editor.document.uri.toString() },
309 position,
310 });
311
312 if (expanded == null) return 'Not available';
313
314 return codeFormat(expanded);
315 }
316
317 get onDidChange(): vscode.Event<vscode.Uri> {
318 return this.eventEmitter.event;
319 }
320 }();
321
322 ctx.pushCleanup(
323 vscode.workspace.registerTextDocumentContentProvider(
324 'rust-analyzer',
325 tdcp,
326 ),
327 );
328
329 return async () => {
330 const document = await vscode.workspace.openTextDocument(tdcp.uri);
331 tdcp.eventEmitter.fire(tdcp.uri);
332 return vscode.window.showTextDocument(
333 document,
334 vscode.ViewColumn.Two,
335 true,
336 );
337 };
338}
339
340export function collectGarbage(ctx: Ctx): Cmd {
341 return async () => ctx.client.sendRequest(ra.collectGarbage, null);
342}
343
344export function showReferences(ctx: Ctx): Cmd {
345 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
346 const client = ctx.client;
347 if (client) {
348 vscode.commands.executeCommand(
349 'editor.action.showReferences',
350 vscode.Uri.parse(uri),
351 client.protocol2CodeConverter.asPosition(position),
352 locations.map(client.protocol2CodeConverter.asLocation),
353 );
354 }
355 };
356}
357
358export function applyActionGroup(_ctx: Ctx): Cmd {
359 return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
360 const selectedAction = await vscode.window.showQuickPick(actions);
361 if (!selectedAction) return;
362 await applySnippetWorkspaceEdit(selectedAction.edit);
363 };
364}
365
366export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
367 return async (edit: vscode.WorkspaceEdit) => {
368 await applySnippetWorkspaceEdit(edit);
369 };
370}
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 0937b495c..000000000
--- a/editors/code/src/commands/index.ts
+++ /dev/null
@@ -1,88 +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';
7import { assert } from '../util';
8
9export * from './analyzer_status';
10export * from './matching_brace';
11export * from './join_lines';
12export * from './on_enter';
13export * from './parent_module';
14export * from './syntax_tree';
15export * from './expand_macro';
16export * from './runnables';
17export * from './ssr';
18export * from './server_version';
19
20export function collectGarbage(ctx: Ctx): Cmd {
21 return async () => ctx.client.sendRequest(ra.collectGarbage, null);
22}
23
24export function showReferences(ctx: Ctx): Cmd {
25 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
26 const client = ctx.client;
27 if (client) {
28 vscode.commands.executeCommand(
29 'editor.action.showReferences',
30 vscode.Uri.parse(uri),
31 client.protocol2CodeConverter.asPosition(position),
32 locations.map(client.protocol2CodeConverter.asLocation),
33 );
34 }
35 };
36}
37
38export function applySourceChange(ctx: Ctx): Cmd {
39 return async (change: ra.SourceChange) => {
40 await sourceChange.applySourceChange(ctx, change);
41 };
42}
43
44export function selectAndApplySourceChange(ctx: Ctx): Cmd {
45 return async (changes: ra.SourceChange[]) => {
46 if (changes.length === 1) {
47 await sourceChange.applySourceChange(ctx, changes[0]);
48 } else if (changes.length > 0) {
49 const selectedChange = await vscode.window.showQuickPick(changes);
50 if (!selectedChange) return;
51 await sourceChange.applySourceChange(ctx, selectedChange);
52 }
53 };
54}
55
56export function applySnippetWorkspaceEdit(_ctx: Ctx): Cmd {
57 return async (edit: vscode.WorkspaceEdit) => {
58 assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
59 const [uri, edits] = edit.entries()[0];
60
61 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
62 if (!editor) return;
63
64 let editWithSnippet: vscode.TextEdit | undefined = undefined;
65 let lineDelta = 0;
66 await editor.edit((builder) => {
67 for (const indel of edits) {
68 const isSnippet = indel.newText.indexOf('$0') !== -1 || indel.newText.indexOf('${') !== -1;
69 if (isSnippet) {
70 editWithSnippet = indel;
71 } else {
72 if (!editWithSnippet) {
73 lineDelta = (indel.newText.match(/\n/g) || []).length - (indel.range.end.line - indel.range.start.line);
74 }
75 builder.replace(indel.range, indel.newText);
76 }
77 }
78 });
79 if (editWithSnippet) {
80 const snip = editWithSnippet as vscode.TextEdit;
81 const range = snip.range.with(
82 snip.range.start.with(snip.range.start.line + lineDelta),
83 snip.range.end.with(snip.range.end.line + lineDelta),
84 );
85 await editor.insertSnippet(new vscode.SnippetString(snip.newText), range);
86 }
87 };
88}
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/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 4ef8cdf04..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 ee294fbe3..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",
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts
index d3fe588e8..027504ecd 100644
--- a/editors/code/src/debug.ts
+++ b/editors/code/src/debug.ts
@@ -1,7 +1,7 @@
1import * as os from "os"; 1import * as os from "os";
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import * as path from 'path'; 3import * as path from 'path';
4import * as ra from './rust-analyzer-api'; 4import * as ra from './lsp_ext';
5 5
6import { Cargo } from './cargo'; 6import { Cargo } from './cargo';
7import { Ctx } from "./ctx"; 7import { Ctx } from "./ctx";
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index a2b07d003..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';
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
new file mode 100644
index 000000000..4da12eb30
--- /dev/null
+++ b/editors/code/src/lsp_ext.ts
@@ -0,0 +1,84 @@
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}
48export interface Runnable {
49 range: lc.Range;
50 label: string;
51 bin: string;
52 args: string[];
53 extraArgs: string[];
54 env: { [key: string]: string };
55 cwd: string | null;
56}
57export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("rust-analyzer/runnables");
58
59export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
60
61export namespace InlayHint {
62 export const enum Kind {
63 TypeHint = "TypeHint",
64 ParamHint = "ParameterHint",
65 ChainingHint = "ChainingHint",
66 }
67 interface Common {
68 range: lc.Range;
69 label: string;
70 }
71 export type TypeHint = Common & { kind: Kind.TypeHint };
72 export type ParamHint = Common & { kind: Kind.ParamHint };
73 export type ChainingHint = Common & { kind: Kind.ChainingHint };
74}
75export interface InlayHintsParams {
76 textDocument: lc.TextDocumentIdentifier;
77}
78export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void>("rust-analyzer/inlayHints");
79
80export interface SsrParams {
81 query: string;
82 parseOnly: boolean;
83}
84export 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 ac3bb365e..31ac81ee8 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -1,7 +1,7 @@
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';
@@ -12,6 +12,7 @@ import { 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 { activateTaskProvider } from './tasks'; 14import { activateTaskProvider } from './tasks';
15import { exec } from 'child_process';
15 16
16let ctx: Ctx | undefined; 17let ctx: Ctx | undefined;
17 18
@@ -85,14 +86,14 @@ export async function activate(context: vscode.ExtensionContext) {
85 86
86 ctx.registerCommand('ssr', commands.ssr); 87 ctx.registerCommand('ssr', commands.ssr);
87 ctx.registerCommand('serverVersion', commands.serverVersion); 88 ctx.registerCommand('serverVersion', commands.serverVersion);
89 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
88 90
89 // Internal commands which are invoked by the server. 91 // Internal commands which are invoked by the server.
90 ctx.registerCommand('runSingle', commands.runSingle); 92 ctx.registerCommand('runSingle', commands.runSingle);
91 ctx.registerCommand('debugSingle', commands.debugSingle); 93 ctx.registerCommand('debugSingle', commands.debugSingle);
92 ctx.registerCommand('showReferences', commands.showReferences); 94 ctx.registerCommand('showReferences', commands.showReferences);
93 ctx.registerCommand('applySourceChange', commands.applySourceChange); 95 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
94 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEdit); 96 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
95 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
96 97
97 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 98 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
98 99
@@ -188,6 +189,46 @@ async function bootstrapServer(config: Config, state: PersistentState): Promise<
188 return path; 189 return path;
189} 190}
190 191
192async function patchelf(dest: PathLike): Promise<void> {
193 await vscode.window.withProgress(
194 {
195 location: vscode.ProgressLocation.Notification,
196 title: "Patching rust-analyzer for NixOS"
197 },
198 async (progress, _) => {
199 const expression = `
200 {src, pkgs ? import <nixpkgs> {}}:
201 pkgs.stdenv.mkDerivation {
202 name = "rust-analyzer";
203 inherit src;
204 phases = [ "installPhase" "fixupPhase" ];
205 installPhase = "cp $src $out";
206 fixupPhase = ''
207 chmod 755 $out
208 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
209 '';
210 }
211 `;
212 const origFile = dest + "-orig";
213 await fs.rename(dest, origFile);
214 progress.report({ message: "Patching executable", increment: 20 });
215 await new Promise((resolve, reject) => {
216 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
217 (err, stdout, stderr) => {
218 if (err != null) {
219 reject(Error(stderr));
220 } else {
221 resolve(stdout);
222 }
223 });
224 handle.stdin?.write(expression);
225 handle.stdin?.end();
226 });
227 await fs.unlink(origFile);
228 }
229 );
230}
231
191async function getServer(config: Config, state: PersistentState): Promise<string | undefined> { 232async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
192 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; 233 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
193 if (explicitPath) { 234 if (explicitPath) {
@@ -237,6 +278,12 @@ async function getServer(config: Config, state: PersistentState): Promise<string
237 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); 278 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
238 279
239 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 }); 280 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
281
282 // Patching executable if that's NixOS.
283 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
284 await patchelf(dest);
285 }
286
240 await state.updateServerVersion(config.package.version); 287 await state.updateServerVersion(config.package.version);
241 return dest; 288 return dest;
242} 289}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/run.ts
index 0bd30fb07..2a7a429cf 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/run.ts
@@ -1,13 +1,13 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3import * as ra from '../rust-analyzer-api'; 3import * as ra from './lsp_ext';
4 4
5import { Ctx, Cmd } from '../ctx'; 5import { Ctx, Cmd } from './ctx';
6import { startDebugSession, getDebugConfiguration } from '../debug'; 6import { startDebugSession, getDebugConfiguration } from './debug';
7 7
8const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; 8const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
9 9
10async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> { 10export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
11 const editor = ctx.activeRustEditor; 11 const editor = ctx.activeRustEditor;
12 const client = ctx.client; 12 const client = ctx.client;
13 if (!editor || !client) return; 13 if (!editor || !client) return;
@@ -83,20 +83,6 @@ async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debugg
83 }); 83 });
84} 84}
85 85
86export function run(ctx: Ctx): Cmd {
87 let prevRunnable: RunnableQuickPick | undefined;
88
89 return async () => {
90 const item = await selectRunnable(ctx, prevRunnable);
91 if (!item) return;
92
93 item.detail = 'rerun';
94 prevRunnable = item;
95 const task = createTask(item.runnable);
96 return await vscode.tasks.executeTask(task);
97 };
98}
99
100export function runSingle(ctx: Ctx): Cmd { 86export function runSingle(ctx: Ctx): Cmd {
101 return async (runnable: ra.Runnable) => { 87 return async (runnable: ra.Runnable) => {
102 const editor = ctx.activeRustEditor; 88 const editor = ctx.activeRustEditor;
@@ -165,7 +151,7 @@ export function newDebugConfig(ctx: Ctx): Cmd {
165 }; 151 };
166} 152}
167 153
168class RunnableQuickPick implements vscode.QuickPickItem { 154export class RunnableQuickPick implements vscode.QuickPickItem {
169 public label: string; 155 public label: string;
170 public description?: string | undefined; 156 public description?: string | undefined;
171 public detail?: string | undefined; 157 public detail?: string | undefined;
@@ -184,7 +170,7 @@ interface CargoTaskDefinition extends vscode.TaskDefinition {
184 env?: { [key: string]: string }; 170 env?: { [key: string]: string };
185} 171}
186 172
187function createTask(spec: ra.Runnable): vscode.Task { 173export function createTask(spec: ra.Runnable): vscode.Task {
188 const TASK_SOURCE = 'Rust'; 174 const TASK_SOURCE = 'Rust';
189 const definition: CargoTaskDefinition = { 175 const definition: CargoTaskDefinition = {
190 type: 'cargo', 176 type: 'cargo',
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}