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 { substituteVariablesInEnv } from "./config";
9 import { outputChannel, traceOutputChannel } from "./main";
10 import { randomUUID } from "crypto";
12 export interface Env {
13 [name: string]: string;
16 // Command URIs have a form of command:command-name?arguments, where
17 // arguments is a percent-encoded array of data we want to pass along to
18 // the command function. For "Show References" this is a list of all file
19 // URIs with locations of every reference, and it can get quite long.
21 // To work around it we use an intermediary linkToCommand command. When
22 // we render a command link, a reference to a command with all its arguments
23 // is stored in a map, and instead a linkToCommand link is rendered
24 // with the key to that map.
25 export const LINKED_COMMANDS = new Map<string, ra.CommandLink>();
27 // For now the map is cleaned up periodically (I've set it to every
28 // 10 minutes). In general case we'll probably need to introduce TTLs or
29 // flags to denote ephemeral links (like these in hover popups) and
30 // persistent links and clean those separately. But for now simply keeping
31 // the last few links in the map should be good enough. Likewise, we could
32 // add code to remove a target command from the map after the link is
33 // clicked, but assuming most links in hover sheets won't be clicked anyway
34 // this code won't change the overall memory use much.
35 setInterval(function cleanupOlderCommandLinks() {
36 // keys are returned in insertion order, we'll keep a few
37 // of recent keys available, and clean the rest
38 const keys = [...LINKED_COMMANDS.keys()];
39 const keysToRemove = keys.slice(0, keys.length - 10);
40 for (const key of keysToRemove) {
41 LINKED_COMMANDS.delete(key);
45 function renderCommand(cmd: ra.CommandLink): string {
46 const commandId = randomUUID();
47 LINKED_COMMANDS.set(commandId, cmd);
48 return `[${cmd.title}](command:rust-analyzer.linkToCommand?${encodeURIComponent(
49 JSON.stringify([commandId])
50 )} '${cmd.tooltip}')`;
53 function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString {
57 (group.title ? group.title + " " : "") +
58 group.commands.map(renderCommand).join(" | ")
62 const result = new vscode.MarkdownString(text);
63 result.isTrusted = true;
67 export async function createClient(
71 ): Promise<lc.LanguageClient> {
72 // '.' Is the fallback if no folder is open
73 // TODO?: Workspace folders support Uri's (eg: file://test.txt).
74 // It might be a good idea to test if the uri points to a file.
76 const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
77 const run: lc.Executable = {
79 options: { env: newEnv },
81 const serverOptions: lc.ServerOptions = {
86 let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
88 if (workspace.kind === "Detached Files") {
89 initializationOptions = {
90 detachedFiles: workspace.files.map((file) => file.uri.fsPath),
91 ...initializationOptions,
95 const clientOptions: lc.LanguageClientOptions = {
96 documentSelector: [{ scheme: "file", language: "rust" }],
97 initializationOptions,
98 diagnosticCollectionName: "rustc",
99 traceOutputChannel: traceOutputChannel(),
100 outputChannel: outputChannel(),
103 document: vscode.TextDocument,
104 position: vscode.Position,
105 token: vscode.CancellationToken,
106 _next: lc.ProvideHoverSignature
108 const editor = vscode.window.activeTextEditor;
109 const positionOrRange = editor?.selection?.contains(position)
110 ? client.code2ProtocolConverter.asRange(editor.selection)
111 : client.code2ProtocolConverter.asPosition(position);
117 client.code2ProtocolConverter.asTextDocumentIdentifier(document),
118 position: positionOrRange,
124 const hover = client.protocol2CodeConverter.asHover(result);
126 const actions = (<any>result).actions;
128 hover.contents.push(renderHoverActions(actions));
134 client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
135 return Promise.resolve(null);
139 // Using custom handling of CodeActions to support action groups and snippet edits.
140 // Note that this means we have to re-implement lazy edit resolving ourselves as well.
141 async provideCodeActions(
142 document: vscode.TextDocument,
144 context: vscode.CodeActionContext,
145 token: vscode.CancellationToken,
146 _next: lc.ProvideCodeActionsSignature
148 const params: lc.CodeActionParams = {
149 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
150 range: client.code2ProtocolConverter.asRange(range),
151 context: await client.code2ProtocolConverter.asCodeActionContext(
156 return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
158 if (values === null) return undefined;
159 const result: (vscode.CodeAction | vscode.Command)[] = [];
160 const groups = new Map<
162 { index: number; items: vscode.CodeAction[] }
164 for (const item of values) {
165 // In our case we expect to get code edits only from diagnostics
166 if (lc.CodeAction.is(item)) {
169 "We don't expect to receive commands in CodeActions"
171 const action = await client.protocol2CodeConverter.asCodeAction(
179 isCodeActionWithoutEditsAndCommands(item),
180 "We don't expect edits or commands here"
182 const kind = client.protocol2CodeConverter.asCodeActionKind(
185 const action = new vscode.CodeAction(item.title, kind);
186 const group = (item as any).group;
188 command: "rust-analyzer.resolveCodeAction",
193 // Set a dummy edit, so that VS Code doesn't try to resolve this.
194 action.edit = new WorkspaceEdit();
197 let entry = groups.get(group);
199 entry = { index: result.length, items: [] };
200 groups.set(group, entry);
203 entry.items.push(action);
208 for (const [group, { index, items }] of groups) {
209 if (items.length === 1) {
210 result[index] = items[0];
212 const action = new vscode.CodeAction(group);
213 action.kind = items[0].kind;
215 command: "rust-analyzer.applyActionGroup",
218 items.map((item) => {
221 arguments: item.command!.arguments![0],
227 // Set a dummy edit, so that VS Code doesn't try to resolve this.
228 action.edit = new WorkspaceEdit();
230 result[index] = action;
235 (_error) => undefined
244 const client = new lc.LanguageClient(
246 "Rust Analyzer Language Server",
251 // To turn on all proposed features use: client.registerProposedFeatures();
252 client.registerFeature(new ExperimentalFeatures());
257 class ExperimentalFeatures implements lc.StaticFeature {
258 fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
259 const caps: any = capabilities.experimental ?? {};
260 caps.snippetTextEdit = true;
261 caps.codeActionGroup = true;
262 caps.hoverActions = true;
263 caps.serverStatusNotification = true;
266 "rust-analyzer.runSingle",
267 "rust-analyzer.debugSingle",
268 "rust-analyzer.showReferences",
269 "rust-analyzer.gotoLocation",
270 "editor.action.triggerParameterHints",
273 capabilities.experimental = caps;
276 _capabilities: lc.ServerCapabilities<any>,
277 _documentSelector: lc.DocumentSelector | undefined
282 function isCodeActionWithoutEditsAndCommands(value: any): boolean {
283 const candidate: lc.CodeAction = value;
286 Is.string(candidate.title) &&
287 (candidate.diagnostics === void 0 ||
288 Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) &&
289 (candidate.kind === void 0 || Is.string(candidate.kind)) &&
290 candidate.edit === void 0 &&
291 candidate.command === void 0