aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/cargo.ts53
-rw-r--r--editors/code/src/client.ts98
-rw-r--r--editors/code/src/commands/index.ts70
-rw-r--r--editors/code/src/commands/join_lines.ts12
-rw-r--r--editors/code/src/commands/on_enter.ts5
-rw-r--r--editors/code/src/commands/ssr.ts6
-rw-r--r--editors/code/src/main.ts52
-rw-r--r--editors/code/src/rust-analyzer-api.ts8
8 files changed, 255 insertions, 49 deletions
diff --git a/editors/code/src/cargo.ts b/editors/code/src/cargo.ts
index 28c7de992..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,26 +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 if (cargoArgs[0] === "run") {
55 cargoArgs[0] = "build";
56 } else if (cargoArgs.indexOf("--no-run") === -1) {
57 cargoArgs.push("--no-run");
58 }
59
60 let artifacts = await this.artifactsFromArgs(cargoArgs);
61 if (cargoArgs[0] === "test") {
62 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
63 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
64 artifacts = artifacts.filter(a => a.isTest);
65 }
66 81
67 if (artifacts.length === 0) { 82 if (artifacts.length === 0) {
68 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 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/commands/index.ts b/editors/code/src/commands/index.ts
index bdb7fc3b0..abb53a248 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -4,6 +4,7 @@ import * as ra from '../rust-analyzer-api';
4 4
5import { Ctx, Cmd } from '../ctx'; 5import { Ctx, Cmd } from '../ctx';
6import * as sourceChange from '../source_change'; 6import * as sourceChange from '../source_change';
7import { assert } from '../util';
7 8
8export * from './analyzer_status'; 9export * from './analyzer_status';
9export * from './matching_brace'; 10export * from './matching_brace';
@@ -40,14 +41,65 @@ export function applySourceChange(ctx: Ctx): Cmd {
40 }; 41 };
41} 42}
42 43
43export function selectAndApplySourceChange(ctx: Ctx): Cmd { 44export function applyActionGroup(_ctx: Ctx): Cmd {
44 return async (changes: ra.SourceChange[]) => { 45 return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
45 if (changes.length === 1) { 46 const selectedAction = await vscode.window.showQuickPick(actions);
46 await sourceChange.applySourceChange(ctx, changes[0]); 47 if (!selectedAction) return;
47 } else if (changes.length > 0) { 48 await applySnippetWorkspaceEdit(selectedAction.edit);
48 const selectedChange = await vscode.window.showQuickPick(changes); 49 };
49 if (!selectedChange) return; 50}
50 await sourceChange.applySourceChange(ctx, selectedChange); 51
51 } 52export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
53 return async (edit: vscode.WorkspaceEdit) => {
54 await applySnippetWorkspaceEdit(edit);
52 }; 55 };
53} 56}
57
58export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
59 assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
60 const [uri, edits] = edit.entries()[0];
61
62 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
63 if (!editor) return;
64
65 let selection: vscode.Selection | undefined = undefined;
66 let lineDelta = 0;
67 await editor.edit((builder) => {
68 for (const indel of edits) {
69 const parsed = parseSnippet(indel.newText);
70 if (parsed) {
71 const [newText, [placeholderStart, placeholderLength]] = parsed;
72 const prefix = newText.substr(0, placeholderStart);
73 const lastNewline = prefix.lastIndexOf('\n');
74
75 const startLine = indel.range.start.line + lineDelta + countLines(prefix);
76 const startColumn = lastNewline === -1 ?
77 indel.range.start.character + placeholderStart
78 : prefix.length - lastNewline - 1;
79 const endColumn = startColumn + placeholderLength;
80 selection = new vscode.Selection(
81 new vscode.Position(startLine, startColumn),
82 new vscode.Position(startLine, endColumn),
83 );
84 builder.replace(indel.range, newText);
85 } else {
86 lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
87 builder.replace(indel.range, indel.newText);
88 }
89 }
90 });
91 if (selection) editor.selection = selection;
92}
93
94function parseSnippet(snip: string): [string, [number, number]] | undefined {
95 const m = snip.match(/\$(0|\{0:([^}]*)\})/);
96 if (!m) return undefined;
97 const placeholder = m[2] ?? "";
98 const range: [number, number] = [m.index!!, placeholder.length];
99 const insert = snip.replace(m[0], placeholder);
100 return [insert, range];
101}
102
103function countLines(text: string): number {
104 return (text.match(/\n/g) || []).length;
105}
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
index de0614653..0bf1ee6e6 100644
--- a/editors/code/src/commands/join_lines.ts
+++ b/editors/code/src/commands/join_lines.ts
@@ -1,7 +1,7 @@
1import * as ra from '../rust-analyzer-api'; 1import * as ra from '../rust-analyzer-api';
2import * as lc from 'vscode-languageclient';
2 3
3import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd } from '../ctx';
4import { applySourceChange } from '../source_change';
5 5
6export function joinLines(ctx: Ctx): Cmd { 6export function joinLines(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
@@ -9,10 +9,14 @@ export function joinLines(ctx: Ctx): Cmd {
9 const client = ctx.client; 9 const client = ctx.client;
10 if (!editor || !client) return; 10 if (!editor || !client) return;
11 11
12 const change = await client.sendRequest(ra.joinLines, { 12 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
13 range: client.code2ProtocolConverter.asRange(editor.selection), 13 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
14 textDocument: { uri: editor.document.uri.toString() }, 14 textDocument: { uri: editor.document.uri.toString() },
15 }); 15 });
16 await applySourceChange(ctx, change); 16 editor.edit((builder) => {
17 client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
18 builder.replace(edit.range, edit.newText);
19 });
20 });
17 }; 21 };
18} 22}
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts
index 285849db7..a7871c31e 100644
--- a/editors/code/src/commands/on_enter.ts
+++ b/editors/code/src/commands/on_enter.ts
@@ -1,8 +1,8 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api'; 2import * as ra from '../rust-analyzer-api';
3 3
4import { applySourceChange } from '../source_change';
5import { Cmd, Ctx } from '../ctx'; 4import { Cmd, Ctx } from '../ctx';
5import { applySnippetWorkspaceEdit } from '.';
6 6
7async function handleKeypress(ctx: Ctx) { 7async function handleKeypress(ctx: Ctx) {
8 const editor = ctx.activeRustEditor; 8 const editor = ctx.activeRustEditor;
@@ -21,7 +21,8 @@ async function handleKeypress(ctx: Ctx) {
21 }); 21 });
22 if (!change) return false; 22 if (!change) return false;
23 23
24 await applySourceChange(ctx, change); 24 const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
25 await applySnippetWorkspaceEdit(workspaceEdit);
25 return true; 26 return true;
26} 27}
27 28
diff --git a/editors/code/src/commands/ssr.ts b/editors/code/src/commands/ssr.ts
index 4ef8cdf04..5d40a64d2 100644
--- a/editors/code/src/commands/ssr.ts
+++ b/editors/code/src/commands/ssr.ts
@@ -2,7 +2,6 @@ import * as vscode from 'vscode';
2import * as ra from "../rust-analyzer-api"; 2import * as ra from "../rust-analyzer-api";
3 3
4import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd } from '../ctx';
5import { applySourceChange } from '../source_change';
6 5
7export function ssr(ctx: Ctx): Cmd { 6export function ssr(ctx: Ctx): Cmd {
8 return async () => { 7 return async () => {
@@ -22,11 +21,10 @@ export function ssr(ctx: Ctx): Cmd {
22 } 21 }
23 }; 22 };
24 const request = await vscode.window.showInputBox(options); 23 const request = await vscode.window.showInputBox(options);
25
26 if (!request) return; 24 if (!request) return;
27 25
28 const change = await client.sendRequest(ra.ssr, { query: request, parseOnly: false }); 26 const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
29 27
30 await applySourceChange(ctx, change); 28 await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
31 }; 29 };
32} 30}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index c015460b8..3405634f3 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
@@ -91,7 +92,8 @@ export async function activate(context: vscode.ExtensionContext) {
91 ctx.registerCommand('debugSingle', commands.debugSingle); 92 ctx.registerCommand('debugSingle', commands.debugSingle);
92 ctx.registerCommand('showReferences', commands.showReferences); 93 ctx.registerCommand('showReferences', commands.showReferences);
93 ctx.registerCommand('applySourceChange', commands.applySourceChange); 94 ctx.registerCommand('applySourceChange', commands.applySourceChange);
94 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 95 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
96 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
95 97
96 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 98 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
97 99
@@ -187,6 +189,46 @@ async function bootstrapServer(config: Config, state: PersistentState): Promise<
187 return path; 189 return path;
188} 190}
189 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
190async function getServer(config: Config, state: PersistentState): Promise<string | undefined> { 232async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
191 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; 233 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
192 if (explicitPath) { 234 if (explicitPath) {
@@ -236,6 +278,12 @@ async function getServer(config: Config, state: PersistentState): Promise<string
236 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); 278 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
237 279
238 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
239 await state.updateServerVersion(config.package.version); 287 await state.updateServerVersion(config.package.version);
240 return dest; 288 return dest;
241} 289}
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 400ac3714..73f36432f 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -64,12 +64,12 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
64 64
65export interface JoinLinesParams { 65export interface JoinLinesParams {
66 textDocument: lc.TextDocumentIdentifier; 66 textDocument: lc.TextDocumentIdentifier;
67 range: lc.Range; 67 ranges: lc.Range[];
68} 68}
69export const joinLines = request<JoinLinesParams, SourceChange>("joinLines"); 69export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
70 70
71 71
72export const onEnter = request<lc.TextDocumentPositionParams, Option<SourceChange>>("onEnter"); 72export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
73 73
74export interface RunnablesParams { 74export interface RunnablesParams {
75 textDocument: lc.TextDocumentIdentifier; 75 textDocument: lc.TextDocumentIdentifier;
@@ -112,7 +112,7 @@ export interface SsrParams {
112 query: string; 112 query: string;
113 parseOnly: boolean; 113 parseOnly: boolean;
114} 114}
115export const ssr = request<SsrParams, SourceChange>("ssr"); 115export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, unknown>('experimental/ssr');
116 116
117 117
118export const publishDecorations = notification<PublishDecorationsParams>("publishDecorations"); 118export const publishDecorations = notification<PublishDecorationsParams>("publishDecorations");