]> git.lizzy.rs Git - rust.git/blob - editors/code/src/util.ts
08159b43c3e1ebb62a4ca87b59b666b5c826d74a
[rust.git] / editors / code / src / util.ts
1 import * as lc from "vscode-languageclient/node";
2 import * as vscode from "vscode";
3 import { strict as nativeAssert } from "assert";
4 import { spawnSync } from "child_process";
5 import { inspect } from "util";
6
7 export function assert(condition: boolean, explanation: string): asserts condition {
8     try {
9         nativeAssert(condition, explanation);
10     } catch (err) {
11         log.error(`Assertion failed:`, explanation);
12         throw err;
13     }
14 }
15
16 export const log = new class {
17     private enabled = true;
18     private readonly output = vscode.window.createOutputChannel("Rust Analyzer Client");
19
20     setEnabled(yes: boolean): void {
21         log.enabled = yes;
22     }
23
24     // Hint: the type [T, ...T[]] means a non-empty array
25     debug(...msg: [unknown, ...unknown[]]): void {
26         if (!log.enabled) return;
27         log.write("DEBUG", ...msg);
28     }
29
30     info(...msg: [unknown, ...unknown[]]): void {
31         log.write("INFO", ...msg);
32     }
33
34     warn(...msg: [unknown, ...unknown[]]): void {
35         debugger;
36         log.write("WARN", ...msg);
37     }
38
39     error(...msg: [unknown, ...unknown[]]): void {
40         debugger;
41         log.write("ERROR", ...msg);
42         log.output.show(true);
43     }
44
45     private write(label: string, ...messageParts: unknown[]): void {
46         const message = messageParts.map(log.stringify).join(" ");
47         const dateTime = new Date().toLocaleString();
48         log.output.appendLine(`${label} [${dateTime}]: ${message}`);
49     }
50
51     private stringify(val: unknown): string {
52         if (typeof val === "string") return val;
53         return inspect(val, {
54             colors: false,
55             depth: 6, // heuristic
56         });
57     }
58 };
59
60 export async function sendRequestWithRetry<TParam, TRet>(
61     client: lc.LanguageClient,
62     reqType: lc.RequestType<TParam, TRet, unknown>,
63     param: TParam,
64     token?: vscode.CancellationToken,
65 ): Promise<TRet> {
66     // The sequence is `10 * (2 ** (2 * n))` where n is 1, 2, 3...
67     for (const delay of [40, 160, 640, 2560, 10240, null]) {
68         try {
69             return await (token
70                 ? client.sendRequest(reqType, param, token)
71                 : client.sendRequest(reqType, param)
72             );
73         } catch (error) {
74             if (delay === null) {
75                 log.warn("LSP request timed out", { method: reqType.method, param, error });
76                 throw error;
77             }
78
79             if (error.code === lc.ErrorCodes.RequestCancelled) {
80                 throw error;
81             }
82
83             if (error.code !== lc.ErrorCodes.ContentModified) {
84                 log.warn("LSP request failed", { method: reqType.method, param, error });
85                 throw error;
86             }
87             await sleep(delay);
88         }
89     }
90     throw 'unreachable';
91 }
92
93 export function sleep(ms: number) {
94     return new Promise(resolve => setTimeout(resolve, ms));
95 }
96
97 export type RustDocument = vscode.TextDocument & { languageId: "rust" };
98 export type RustEditor = vscode.TextEditor & { document: RustDocument };
99
100 export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
101     // Prevent corrupted text (particularly via inlay hints) in diff views
102     // by allowing only `file` schemes
103     // unfortunately extensions that use diff views not always set this
104     // to something different than 'file' (see ongoing bug: #4608)
105     return document.languageId === 'rust' && document.uri.scheme === 'file';
106 }
107
108 export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
109     return isRustDocument(editor.document);
110 }
111
112 export function isValidExecutable(path: string): boolean {
113     log.debug("Checking availability of a binary at", path);
114
115     const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
116
117     const printOutput = res.error && (res.error as any).code !== 'ENOENT' ? log.warn : log.debug;
118     printOutput(path, "--version:", res);
119
120     return res.status === 0;
121 }
122
123 /** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */
124 export function setContextValue(key: string, value: any): Thenable<void> {
125     return vscode.commands.executeCommand('setContext', key, value);
126 }
127
128 /**
129  * Returns a higher-order function that caches the results of invoking the
130  * underlying function.
131  */
132 export function memoize<Ret, TThis, Param extends string>(func: (this: TThis, arg: Param) => Ret) {
133     const cache = new Map<string, Ret>();
134
135     return function(this: TThis, arg: Param) {
136         const cached = cache.get(arg);
137         if (cached) return cached;
138
139         const result = func.call(this, arg);
140         cache.set(arg, result);
141
142         return result;
143     };
144 }