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(),
109 document: vscode.TextDocument,
110 position: vscode.Position,
111 token: vscode.CancellationToken,
112 _next: lc.ProvideHoverSignature
114 const editor = vscode.window.activeTextEditor;
115 const positionOrRange = editor?.selection?.contains(position)
116 ? client.code2ProtocolConverter.asRange(editor.selection)
117 : client.code2ProtocolConverter.asPosition(position);
123 client.code2ProtocolConverter.asTextDocumentIdentifier(document),
124 position: positionOrRange,
130 const hover = client.protocol2CodeConverter.asHover(result);
132 const actions = (<any>result).actions;
134 hover.contents.push(renderHoverActions(actions));
140 client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
141 return Promise.resolve(null);
145 // Using custom handling of CodeActions to support action groups and snippet edits.
146 // Note that this means we have to re-implement lazy edit resolving ourselves as well.
147 async provideCodeActions(
148 document: vscode.TextDocument,
150 context: vscode.CodeActionContext,
151 token: vscode.CancellationToken,
152 _next: lc.ProvideCodeActionsSignature
154 const params: lc.CodeActionParams = {
155 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
156 range: client.code2ProtocolConverter.asRange(range),
157 context: await client.code2ProtocolConverter.asCodeActionContext(
162 return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
164 if (values === null) return undefined;
165 const result: (vscode.CodeAction | vscode.Command)[] = [];
166 const groups = new Map<
168 { index: number; items: vscode.CodeAction[] }
170 for (const item of values) {
171 // In our case we expect to get code edits only from diagnostics
172 if (lc.CodeAction.is(item)) {
175 "We don't expect to receive commands in CodeActions"
177 const action = await client.protocol2CodeConverter.asCodeAction(
185 isCodeActionWithoutEditsAndCommands(item),
186 "We don't expect edits or commands here"
188 const kind = client.protocol2CodeConverter.asCodeActionKind(
191 const action = new vscode.CodeAction(item.title, kind);
192 const group = (item as any).group;
194 command: "rust-analyzer.resolveCodeAction",
199 // Set a dummy edit, so that VS Code doesn't try to resolve this.
200 action.edit = new WorkspaceEdit();
203 let entry = groups.get(group);
205 entry = { index: result.length, items: [] };
206 groups.set(group, entry);
209 entry.items.push(action);
214 for (const [group, { index, items }] of groups) {
215 if (items.length === 1) {
216 result[index] = items[0];
218 const action = new vscode.CodeAction(group);
219 action.kind = items[0].kind;
221 command: "rust-analyzer.applyActionGroup",
224 items.map((item) => {
227 arguments: item.command!.arguments![0],
233 // Set a dummy edit, so that VS Code doesn't try to resolve this.
234 action.edit = new WorkspaceEdit();
236 result[index] = action;
241 (_error) => undefined
250 const client = new lc.LanguageClient(
252 "Rust Analyzer Language Server",
257 // To turn on all proposed features use: client.registerProposedFeatures();
258 client.registerFeature(new ExperimentalFeatures());
263 class ExperimentalFeatures implements lc.StaticFeature {
264 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
265 const caps: any = capabilities.experimental ?? {};
266 caps.snippetTextEdit = true;
267 caps.codeActionGroup = true;
268 caps.hoverActions = true;
269 caps.serverStatusNotification = true;
272 "rust-analyzer.runSingle",
273 "rust-analyzer.debugSingle",
274 "rust-analyzer.showReferences",
275 "rust-analyzer.gotoLocation",
276 "editor.action.triggerParameterHints",
279 capabilities.experimental = caps;
282 _capabilities: lc.ServerCapabilities<any>,
283 _documentSelector: lc.DocumentSelector | undefined
288 function isCodeActionWithoutEditsAndCommands(value: any): boolean {
289 const candidate: lc.CodeAction = value;
292 Is.string(candidate.title) &&
293 (candidate.diagnostics === void 0 ||
294 Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) &&
295 (candidate.kind === void 0 || Is.string(candidate.kind)) &&
296 candidate.edit === void 0 &&
297 candidate.command === void 0