]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/editors/code/src/client.ts
Rollup merge of #100389 - compiler-errors:return-type-suggestion-cycle, r=cjgillot
[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 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 = " ";
113                     }
114                     if (diagnostic.relatedInformation) {
115                         for (const relatedInformation of diagnostic.relatedInformation) {
116                             if (!relatedInformation.message) {
117                                 relatedInformation.message = " ";
118                             }
119                         }
120                     }
121                 }
122                 next(uri, diagnostics);
123             },
124             async provideHover(
125                 document: vscode.TextDocument,
126                 position: vscode.Position,
127                 token: vscode.CancellationToken,
128                 _next: lc.ProvideHoverSignature
129             ) {
130                 const editor = vscode.window.activeTextEditor;
131                 const positionOrRange = editor?.selection?.contains(position)
132                     ? client.code2ProtocolConverter.asRange(editor.selection)
133                     : client.code2ProtocolConverter.asPosition(position);
134                 return client
135                     .sendRequest(
136                         ra.hover,
137                         {
138                             textDocument:
139                                 client.code2ProtocolConverter.asTextDocumentIdentifier(document),
140                             position: positionOrRange,
141                         },
142                         token
143                     )
144                     .then(
145                         (result) => {
146                             const hover = client.protocol2CodeConverter.asHover(result);
147                             if (hover) {
148                                 const actions = (<any>result).actions;
149                                 if (actions) {
150                                     hover.contents.push(renderHoverActions(actions));
151                                 }
152                             }
153                             return hover;
154                         },
155                         (error) => {
156                             client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
157                             return Promise.resolve(null);
158                         }
159                     );
160             },
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,
165                 range: vscode.Range,
166                 context: vscode.CodeActionContext,
167                 token: vscode.CancellationToken,
168                 _next: lc.ProvideCodeActionsSignature
169             ) {
170                 const params: lc.CodeActionParams = {
171                     textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
172                     range: client.code2ProtocolConverter.asRange(range),
173                     context: await client.code2ProtocolConverter.asCodeActionContext(
174                         context,
175                         token
176                     ),
177                 };
178                 return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
179                     async (values) => {
180                         if (values === null) return undefined;
181                         const result: (vscode.CodeAction | vscode.Command)[] = [];
182                         const groups = new Map<
183                             string,
184                             { index: number; items: vscode.CodeAction[] }
185                         >();
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)) {
189                                 assert(
190                                     !item.command,
191                                     "We don't expect to receive commands in CodeActions"
192                                 );
193                                 const action = await client.protocol2CodeConverter.asCodeAction(
194                                     item,
195                                     token
196                                 );
197                                 result.push(action);
198                                 continue;
199                             }
200                             assert(
201                                 isCodeActionWithoutEditsAndCommands(item),
202                                 "We don't expect edits or commands here"
203                             );
204                             const kind = client.protocol2CodeConverter.asCodeActionKind(
205                                 (item as any).kind
206                             );
207                             const action = new vscode.CodeAction(item.title, kind);
208                             const group = (item as any).group;
209                             action.command = {
210                                 command: "rust-analyzer.resolveCodeAction",
211                                 title: item.title,
212                                 arguments: [item],
213                             };
214
215                             // Set a dummy edit, so that VS Code doesn't try to resolve this.
216                             action.edit = new WorkspaceEdit();
217
218                             if (group) {
219                                 let entry = groups.get(group);
220                                 if (!entry) {
221                                     entry = { index: result.length, items: [] };
222                                     groups.set(group, entry);
223                                     result.push(action);
224                                 }
225                                 entry.items.push(action);
226                             } else {
227                                 result.push(action);
228                             }
229                         }
230                         for (const [group, { index, items }] of groups) {
231                             if (items.length === 1) {
232                                 result[index] = items[0];
233                             } else {
234                                 const action = new vscode.CodeAction(group);
235                                 action.kind = items[0].kind;
236                                 action.command = {
237                                     command: "rust-analyzer.applyActionGroup",
238                                     title: "",
239                                     arguments: [
240                                         items.map((item) => {
241                                             return {
242                                                 label: item.title,
243                                                 arguments: item.command!.arguments![0],
244                                             };
245                                         }),
246                                     ],
247                                 };
248
249                                 // Set a dummy edit, so that VS Code doesn't try to resolve this.
250                                 action.edit = new WorkspaceEdit();
251
252                                 result[index] = action;
253                             }
254                         }
255                         return result;
256                     },
257                     (_error) => undefined
258                 );
259             },
260         },
261         markdown: {
262             supportHtml: true,
263         },
264     };
265
266     const client = new lc.LanguageClient(
267         "rust-analyzer",
268         "Rust Analyzer Language Server",
269         serverOptions,
270         clientOptions
271     );
272
273     // To turn on all proposed features use: client.registerProposedFeatures();
274     client.registerFeature(new ExperimentalFeatures());
275
276     return client;
277 }
278
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;
286         caps.commands = {
287             commands: [
288                 "rust-analyzer.runSingle",
289                 "rust-analyzer.debugSingle",
290                 "rust-analyzer.showReferences",
291                 "rust-analyzer.gotoLocation",
292                 "editor.action.triggerParameterHints",
293             ],
294         };
295         capabilities.experimental = caps;
296     }
297     initialize(
298         _capabilities: lc.ServerCapabilities<any>,
299         _documentSelector: lc.DocumentSelector | undefined
300     ): void {}
301     dispose(): void {}
302 }
303
304 function isCodeActionWithoutEditsAndCommands(value: any): boolean {
305     const candidate: lc.CodeAction = value;
306     return (
307         candidate &&
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
314     );
315 }