1 import * as lc from "vscode-languageclient/node";
2 import * as vscode from "vscode";
3 import * as ra from "../src/lsp_ext";
4 import * as Is from "vscode-languageclient/lib/common/utils/is";
5 import { assert } from "./util";
6 import { WorkspaceEdit } from "vscode";
7 import { Workspace } from "./ctx";
8 import { updateConfig } from "./config";
9 import { substituteVariablesInEnv } from "./config";
10 import { outputChannel, traceOutputChannel } from "./main";
11 import { randomUUID } from "crypto";
13 export interface Env {
14 [name: string]: string;
17 // Command URIs have a form of command:command-name?arguments, where
18 // arguments is a percent-encoded array of data we want to pass along to
19 // the command function. For "Show References" this is a list of all file
20 // URIs with locations of every reference, and it can get quite long.
22 // To work around it we use an intermediary linkToCommand command. When
23 // we render a command link, a reference to a command with all its arguments
24 // is stored in a map, and instead a linkToCommand link is rendered
25 // with the key to that map.
26 export const LINKED_COMMANDS = new Map<string, ra.CommandLink>();
28 // For now the map is cleaned up periodically (I've set it to every
29 // 10 minutes). In general case we'll probably need to introduce TTLs or
30 // flags to denote ephemeral links (like these in hover popups) and
31 // persistent links and clean those separately. But for now simply keeping
32 // the last few links in the map should be good enough. Likewise, we could
33 // add code to remove a target command from the map after the link is
34 // clicked, but assuming most links in hover sheets won't be clicked anyway
35 // this code won't change the overall memory use much.
36 setInterval(function cleanupOlderCommandLinks() {
37 // keys are returned in insertion order, we'll keep a few
38 // of recent keys available, and clean the rest
39 const keys = [...LINKED_COMMANDS.keys()];
40 const keysToRemove = keys.slice(0, keys.length - 10);
41 for (const key of keysToRemove) {
42 LINKED_COMMANDS.delete(key);
46 function renderCommand(cmd: ra.CommandLink): string {
47 const commandId = randomUUID();
48 LINKED_COMMANDS.set(commandId, cmd);
49 return `[${cmd.title}](command:rust-analyzer.linkToCommand?${encodeURIComponent(
50 JSON.stringify([commandId])
51 )} '${cmd.tooltip}')`;
54 function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString {
58 (group.title ? group.title + " " : "") +
59 group.commands.map(renderCommand).join(" | ")
63 const result = new vscode.MarkdownString(text);
64 result.isTrusted = true;
68 export async function createClient(
72 ): Promise<lc.LanguageClient> {
73 // '.' Is the fallback if no folder is open
74 // TODO?: Workspace folders support Uri's (eg: file://test.txt).
75 // It might be a good idea to test if the uri points to a file.
77 const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
78 const run: lc.Executable = {
80 options: { env: newEnv },
82 const serverOptions: lc.ServerOptions = {
87 let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
89 // Update outdated user configs
90 await updateConfig(initializationOptions).catch((err) => {
91 void vscode.window.showErrorMessage(`Failed updating old config keys: ${err.message}`);
94 if (workspace.kind === "Detached Files") {
95 initializationOptions = {
96 detachedFiles: workspace.files.map((file) => file.uri.fsPath),
97 ...initializationOptions,
101 const clientOptions: lc.LanguageClientOptions = {
102 documentSelector: [{ scheme: "file", language: "rust" }],
103 initializationOptions,
104 diagnosticCollectionName: "rustc",
105 traceOutputChannel: traceOutputChannel(),
106 outputChannel: outputChannel(),
108 async handleDiagnostics(uri, diagnostics, next) {
109 // Workaround for https://github.com/microsoft/vscode/issues/155531
110 for (const diagnostic of diagnostics) {
111 if (!diagnostic.message) {
112 diagnostic.message = " ";
114 if (diagnostic.relatedInformation) {
115 for (const relatedInformation of diagnostic.relatedInformation) {
116 if (!relatedInformation.message) {
117 relatedInformation.message = " ";
122 next(uri, diagnostics);
125 document: vscode.TextDocument,
126 position: vscode.Position,
127 token: vscode.CancellationToken,
128 _next: lc.ProvideHoverSignature
130 const editor = vscode.window.activeTextEditor;
131 const positionOrRange = editor?.selection?.contains(position)
132 ? client.code2ProtocolConverter.asRange(editor.selection)
133 : client.code2ProtocolConverter.asPosition(position);
139 client.code2ProtocolConverter.asTextDocumentIdentifier(document),
140 position: positionOrRange,
146 const hover = client.protocol2CodeConverter.asHover(result);
148 const actions = (<any>result).actions;
150 hover.contents.push(renderHoverActions(actions));
156 client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
157 return Promise.resolve(null);
161 // Using custom handling of CodeActions to support action groups and snippet edits.
162 // Note that this means we have to re-implement lazy edit resolving ourselves as well.
163 async provideCodeActions(
164 document: vscode.TextDocument,
166 context: vscode.CodeActionContext,
167 token: vscode.CancellationToken,
168 _next: lc.ProvideCodeActionsSignature
170 const params: lc.CodeActionParams = {
171 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
172 range: client.code2ProtocolConverter.asRange(range),
173 context: await client.code2ProtocolConverter.asCodeActionContext(
178 return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
180 if (values === null) return undefined;
181 const result: (vscode.CodeAction | vscode.Command)[] = [];
182 const groups = new Map<
184 { index: number; items: vscode.CodeAction[] }
186 for (const item of values) {
187 // In our case we expect to get code edits only from diagnostics
188 if (lc.CodeAction.is(item)) {
191 "We don't expect to receive commands in CodeActions"
193 const action = await client.protocol2CodeConverter.asCodeAction(
201 isCodeActionWithoutEditsAndCommands(item),
202 "We don't expect edits or commands here"
204 const kind = client.protocol2CodeConverter.asCodeActionKind(
207 const action = new vscode.CodeAction(item.title, kind);
208 const group = (item as any).group;
210 command: "rust-analyzer.resolveCodeAction",
215 // Set a dummy edit, so that VS Code doesn't try to resolve this.
216 action.edit = new WorkspaceEdit();
219 let entry = groups.get(group);
221 entry = { index: result.length, items: [] };
222 groups.set(group, entry);
225 entry.items.push(action);
230 for (const [group, { index, items }] of groups) {
231 if (items.length === 1) {
232 result[index] = items[0];
234 const action = new vscode.CodeAction(group);
235 action.kind = items[0].kind;
237 command: "rust-analyzer.applyActionGroup",
240 items.map((item) => {
243 arguments: item.command!.arguments![0],
249 // Set a dummy edit, so that VS Code doesn't try to resolve this.
250 action.edit = new WorkspaceEdit();
252 result[index] = action;
257 (_error) => undefined
266 const client = new lc.LanguageClient(
268 "Rust Analyzer Language Server",
273 // To turn on all proposed features use: client.registerProposedFeatures();
274 client.registerFeature(new ExperimentalFeatures());
279 class ExperimentalFeatures implements lc.StaticFeature {
280 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
281 const caps: any = capabilities.experimental ?? {};
282 caps.snippetTextEdit = true;
283 caps.codeActionGroup = true;
284 caps.hoverActions = true;
285 caps.serverStatusNotification = true;
288 "rust-analyzer.runSingle",
289 "rust-analyzer.debugSingle",
290 "rust-analyzer.showReferences",
291 "rust-analyzer.gotoLocation",
292 "editor.action.triggerParameterHints",
295 capabilities.experimental = caps;
298 _capabilities: lc.ServerCapabilities<any>,
299 _documentSelector: lc.DocumentSelector | undefined
304 function isCodeActionWithoutEditsAndCommands(value: any): boolean {
305 const candidate: lc.CodeAction = value;
308 Is.string(candidate.title) &&
309 (candidate.diagnostics === void 0 ||
310 Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) &&
311 (candidate.kind === void 0 || Is.string(candidate.kind)) &&
312 candidate.edit === void 0 &&
313 candidate.command === void 0