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