]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/editors/code/src/client.ts
Rollup merge of #99500 - tmandry:fuchsia-flags, r=petrochenkov
[rust.git] / src / tools / rust-analyzer / editors / code / src / client.ts
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";
12
13 export interface Env {
14     [name: string]: string;
15 }
16
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.
21 //
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>();
27
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);
43     }
44 }, 10 * 60 * 1000);
45
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}')`;
52 }
53
54 function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString {
55     const text = actions
56         .map(
57             (group) =>
58                 (group.title ? group.title + " " : "") +
59                 group.commands.map(renderCommand).join(" | ")
60         )
61         .join("___");
62
63     const result = new vscode.MarkdownString(text);
64     result.isTrusted = true;
65     return result;
66 }
67
68 export async function createClient(
69     serverPath: string,
70     workspace: Workspace,
71     extraEnv: Env
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.
76
77     const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
78     const run: lc.Executable = {
79         command: serverPath,
80         options: { env: newEnv },
81     };
82     const serverOptions: lc.ServerOptions = {
83         run,
84         debug: run,
85     };
86
87     let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
88
89     // Update outdated user configs
90     await updateConfig(initializationOptions).catch((err) => {
91         void vscode.window.showErrorMessage(`Failed updating old config keys: ${err.message}`);
92     });
93
94     if (workspace.kind === "Detached Files") {
95         initializationOptions = {
96             detachedFiles: workspace.files.map((file) => file.uri.fsPath),
97             ...initializationOptions,
98         };
99     }
100
101     const clientOptions: lc.LanguageClientOptions = {
102         documentSelector: [{ scheme: "file", language: "rust" }],
103         initializationOptions,
104         diagnosticCollectionName: "rustc",
105         traceOutputChannel: traceOutputChannel(),
106         outputChannel: outputChannel(),
107         middleware: {
108             async provideHover(
109                 document: vscode.TextDocument,
110                 position: vscode.Position,
111                 token: vscode.CancellationToken,
112                 _next: lc.ProvideHoverSignature
113             ) {
114                 const editor = vscode.window.activeTextEditor;
115                 const positionOrRange = editor?.selection?.contains(position)
116                     ? client.code2ProtocolConverter.asRange(editor.selection)
117                     : client.code2ProtocolConverter.asPosition(position);
118                 return client
119                     .sendRequest(
120                         ra.hover,
121                         {
122                             textDocument:
123                                 client.code2ProtocolConverter.asTextDocumentIdentifier(document),
124                             position: positionOrRange,
125                         },
126                         token
127                     )
128                     .then(
129                         (result) => {
130                             const hover = client.protocol2CodeConverter.asHover(result);
131                             if (hover) {
132                                 const actions = (<any>result).actions;
133                                 if (actions) {
134                                     hover.contents.push(renderHoverActions(actions));
135                                 }
136                             }
137                             return hover;
138                         },
139                         (error) => {
140                             client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
141                             return Promise.resolve(null);
142                         }
143                     );
144             },
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,
149                 range: vscode.Range,
150                 context: vscode.CodeActionContext,
151                 token: vscode.CancellationToken,
152                 _next: lc.ProvideCodeActionsSignature
153             ) {
154                 const params: lc.CodeActionParams = {
155                     textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
156                     range: client.code2ProtocolConverter.asRange(range),
157                     context: await client.code2ProtocolConverter.asCodeActionContext(
158                         context,
159                         token
160                     ),
161                 };
162                 return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
163                     async (values) => {
164                         if (values === null) return undefined;
165                         const result: (vscode.CodeAction | vscode.Command)[] = [];
166                         const groups = new Map<
167                             string,
168                             { index: number; items: vscode.CodeAction[] }
169                         >();
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)) {
173                                 assert(
174                                     !item.command,
175                                     "We don't expect to receive commands in CodeActions"
176                                 );
177                                 const action = await client.protocol2CodeConverter.asCodeAction(
178                                     item,
179                                     token
180                                 );
181                                 result.push(action);
182                                 continue;
183                             }
184                             assert(
185                                 isCodeActionWithoutEditsAndCommands(item),
186                                 "We don't expect edits or commands here"
187                             );
188                             const kind = client.protocol2CodeConverter.asCodeActionKind(
189                                 (item as any).kind
190                             );
191                             const action = new vscode.CodeAction(item.title, kind);
192                             const group = (item as any).group;
193                             action.command = {
194                                 command: "rust-analyzer.resolveCodeAction",
195                                 title: item.title,
196                                 arguments: [item],
197                             };
198
199                             // Set a dummy edit, so that VS Code doesn't try to resolve this.
200                             action.edit = new WorkspaceEdit();
201
202                             if (group) {
203                                 let entry = groups.get(group);
204                                 if (!entry) {
205                                     entry = { index: result.length, items: [] };
206                                     groups.set(group, entry);
207                                     result.push(action);
208                                 }
209                                 entry.items.push(action);
210                             } else {
211                                 result.push(action);
212                             }
213                         }
214                         for (const [group, { index, items }] of groups) {
215                             if (items.length === 1) {
216                                 result[index] = items[0];
217                             } else {
218                                 const action = new vscode.CodeAction(group);
219                                 action.kind = items[0].kind;
220                                 action.command = {
221                                     command: "rust-analyzer.applyActionGroup",
222                                     title: "",
223                                     arguments: [
224                                         items.map((item) => {
225                                             return {
226                                                 label: item.title,
227                                                 arguments: item.command!.arguments![0],
228                                             };
229                                         }),
230                                     ],
231                                 };
232
233                                 // Set a dummy edit, so that VS Code doesn't try to resolve this.
234                                 action.edit = new WorkspaceEdit();
235
236                                 result[index] = action;
237                             }
238                         }
239                         return result;
240                     },
241                     (_error) => undefined
242                 );
243             },
244         },
245         markdown: {
246             supportHtml: true,
247         },
248     };
249
250     const client = new lc.LanguageClient(
251         "rust-analyzer",
252         "Rust Analyzer Language Server",
253         serverOptions,
254         clientOptions
255     );
256
257     // To turn on all proposed features use: client.registerProposedFeatures();
258     client.registerFeature(new ExperimentalFeatures());
259
260     return client;
261 }
262
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;
270         caps.commands = {
271             commands: [
272                 "rust-analyzer.runSingle",
273                 "rust-analyzer.debugSingle",
274                 "rust-analyzer.showReferences",
275                 "rust-analyzer.gotoLocation",
276                 "editor.action.triggerParameterHints",
277             ],
278         };
279         capabilities.experimental = caps;
280     }
281     initialize(
282         _capabilities: lc.ServerCapabilities<any>,
283         _documentSelector: lc.DocumentSelector | undefined
284     ): void {}
285     dispose(): void {}
286 }
287
288 function isCodeActionWithoutEditsAndCommands(value: any): boolean {
289     const candidate: lc.CodeAction = value;
290     return (
291         candidate &&
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
298     );
299 }