]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/editors/code/src/ctx.ts
Rollup merge of #103283 - nbarrios1337:unsafe-impl-suggestions, r=cjgillot
[rust.git] / src / tools / rust-analyzer / editors / code / src / ctx.ts
1 import * as vscode from "vscode";
2 import * as lc from "vscode-languageclient/node";
3 import * as ra from "./lsp_ext";
4
5 import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
6 import { createClient } from "./client";
7 import { isRustEditor, log, RustEditor } from "./util";
8 import { ServerStatusParams } from "./lsp_ext";
9 import { PersistentState } from "./persistent_state";
10 import { bootstrap } from "./bootstrap";
11
12 export type Workspace =
13     | {
14           kind: "Workspace Folder";
15       }
16     | {
17           kind: "Detached Files";
18           files: vscode.TextDocument[];
19       };
20
21 export type CommandFactory = {
22     enabled: (ctx: Ctx) => Cmd;
23     disabled?: (ctx: Ctx) => Cmd;
24 };
25
26 export class Ctx {
27     readonly statusBar: vscode.StatusBarItem;
28     readonly config: Config;
29
30     private client: lc.LanguageClient | undefined;
31     private _serverPath: string | undefined;
32     private traceOutputChannel: vscode.OutputChannel | undefined;
33     private outputChannel: vscode.OutputChannel | undefined;
34     private clientSubscriptions: Disposable[];
35     private state: PersistentState;
36     private commandFactories: Record<string, CommandFactory>;
37     private commandDisposables: Disposable[];
38
39     workspace: Workspace;
40
41     constructor(
42         readonly extCtx: vscode.ExtensionContext,
43         workspace: Workspace,
44         commandFactories: Record<string, CommandFactory>
45     ) {
46         extCtx.subscriptions.push(this);
47         this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
48         this.statusBar.text = "rust-analyzer";
49         this.statusBar.tooltip = "ready";
50         this.statusBar.command = "rust-analyzer.analyzerStatus";
51         this.statusBar.show();
52         this.workspace = workspace;
53         this.clientSubscriptions = [];
54         this.commandDisposables = [];
55         this.commandFactories = commandFactories;
56
57         this.state = new PersistentState(extCtx.globalState);
58         this.config = new Config(extCtx);
59
60         this.updateCommands();
61     }
62
63     dispose() {
64         this.config.dispose();
65         this.statusBar.dispose();
66         void this.disposeClient();
67         this.commandDisposables.forEach((disposable) => disposable.dispose());
68     }
69
70     clientFetcher() {
71         const self = this;
72         return {
73             get client(): lc.LanguageClient | undefined {
74                 return self.client;
75             },
76         };
77     }
78
79     async getClient() {
80         if (!this.traceOutputChannel) {
81             this.traceOutputChannel = vscode.window.createOutputChannel(
82                 "Rust Analyzer Language Server Trace"
83             );
84             this.pushExtCleanup(this.traceOutputChannel);
85         }
86         if (!this.outputChannel) {
87             this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server");
88             this.pushExtCleanup(this.outputChannel);
89         }
90
91         if (!this.client) {
92             this._serverPath = await bootstrap(this.extCtx, this.config, this.state).catch(
93                 (err) => {
94                     let message = "bootstrap error. ";
95
96                     message +=
97                         'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
98                     message +=
99                         'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
100
101                     log.error("Bootstrap error", err);
102                     throw new Error(message);
103                 }
104             );
105             const newEnv = substituteVariablesInEnv(
106                 Object.assign({}, process.env, this.config.serverExtraEnv)
107             );
108             const run: lc.Executable = {
109                 command: this._serverPath,
110                 options: { env: newEnv },
111             };
112             const serverOptions = {
113                 run,
114                 debug: run,
115             };
116
117             let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
118
119             if (this.workspace.kind === "Detached Files") {
120                 rawInitializationOptions = {
121                     detachedFiles: this.workspace.files.map((file) => file.uri.fsPath),
122                     ...rawInitializationOptions,
123                 };
124             }
125
126             const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
127
128             this.client = await createClient(
129                 this.traceOutputChannel,
130                 this.outputChannel,
131                 initializationOptions,
132                 serverOptions
133             );
134             this.pushClientCleanup(
135                 this.client.onNotification(ra.serverStatus, (params) =>
136                     this.setServerStatus(params)
137                 )
138             );
139         }
140         return this.client;
141     }
142
143     async activate() {
144         log.info("Activating language client");
145         const client = await this.getClient();
146         await client.start();
147         this.updateCommands();
148         return client;
149     }
150
151     async deactivate() {
152         log.info("Deactivating language client");
153         await this.client?.stop();
154         this.updateCommands();
155     }
156
157     async stop() {
158         log.info("Stopping language client");
159         await this.disposeClient();
160         this.updateCommands();
161     }
162
163     private async disposeClient() {
164         this.clientSubscriptions?.forEach((disposable) => disposable.dispose());
165         this.clientSubscriptions = [];
166         await this.client?.dispose();
167         this._serverPath = undefined;
168         this.client = undefined;
169     }
170
171     get activeRustEditor(): RustEditor | undefined {
172         const editor = vscode.window.activeTextEditor;
173         return editor && isRustEditor(editor) ? editor : undefined;
174     }
175
176     get extensionPath(): string {
177         return this.extCtx.extensionPath;
178     }
179
180     get subscriptions(): Disposable[] {
181         return this.extCtx.subscriptions;
182     }
183
184     get serverPath(): string | undefined {
185         return this._serverPath;
186     }
187
188     private updateCommands() {
189         this.commandDisposables.forEach((disposable) => disposable.dispose());
190         this.commandDisposables = [];
191         const fetchFactory = (factory: CommandFactory, fullName: string) => {
192             return this.client && this.client.isRunning()
193                 ? factory.enabled
194                 : factory.disabled ||
195                       ((_) => () =>
196                           vscode.window.showErrorMessage(
197                               `command ${fullName} failed: rust-analyzer server is not running`
198                           ));
199         };
200         for (const [name, factory] of Object.entries(this.commandFactories)) {
201             const fullName = `rust-analyzer.${name}`;
202             const callback = fetchFactory(factory, fullName)(this);
203             this.commandDisposables.push(vscode.commands.registerCommand(fullName, callback));
204         }
205     }
206
207     setServerStatus(status: ServerStatusParams) {
208         let icon = "";
209         const statusBar = this.statusBar;
210         switch (status.health) {
211             case "ok":
212                 statusBar.tooltip = status.message ?? "Ready";
213                 statusBar.command = undefined;
214                 statusBar.color = undefined;
215                 statusBar.backgroundColor = undefined;
216                 break;
217             case "warning":
218                 statusBar.tooltip =
219                     (status.message ? status.message + "\n" : "") + "Click to reload.";
220
221                 statusBar.command = "rust-analyzer.reloadWorkspace";
222                 statusBar.color = new vscode.ThemeColor("statusBarItem.warningForeground");
223                 statusBar.backgroundColor = new vscode.ThemeColor(
224                     "statusBarItem.warningBackground"
225                 );
226                 icon = "$(warning) ";
227                 break;
228             case "error":
229                 statusBar.tooltip =
230                     (status.message ? status.message + "\n" : "") + "Click to reload.";
231
232                 statusBar.command = "rust-analyzer.reloadWorkspace";
233                 statusBar.color = new vscode.ThemeColor("statusBarItem.errorForeground");
234                 statusBar.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground");
235                 icon = "$(error) ";
236                 break;
237         }
238         if (!status.quiescent) icon = "$(sync~spin) ";
239         statusBar.text = `${icon}rust-analyzer`;
240     }
241
242     pushExtCleanup(d: Disposable) {
243         this.extCtx.subscriptions.push(d);
244     }
245
246     private pushClientCleanup(d: Disposable) {
247         this.clientSubscriptions.push(d);
248     }
249 }
250
251 export interface Disposable {
252     dispose(): void;
253 }
254 export type Cmd = (...args: any[]) => unknown;