1import * as lc from "vscode-languageclient/node"; 2import * as vscode from "vscode"; 3import { strict as nativeAssert } from "assert"; 4import { exec, ExecOptions, spawnSync } from "child_process"; 5import { inspect } from "util"; 6 7export 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 16export 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 60export 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 } catch (error) { 73 if (delay === null) { 74 log.warn("LSP request timed out", { method: reqType.method, param, error }); 75 throw error; 76 } 77 if (error.code === lc.LSPErrorCodes.RequestCancelled) { 78 throw error; 79 } 80 81 if (error.code !== lc.LSPErrorCodes.ContentModified) { 82 log.warn("LSP request failed", { method: reqType.method, param, error }); 83 throw error; 84 } 85 await sleep(delay); 86 } 87 } 88 throw "unreachable"; 89} 90 91export function sleep(ms: number) { 92 return new Promise((resolve) => setTimeout(resolve, ms)); 93} 94 95export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 96export type RustEditor = vscode.TextEditor & { document: RustDocument }; 97 98export function isRustDocument(document: vscode.TextDocument): document is RustDocument { 99 // Prevent corrupted text (particularly via inlay hints) in diff views 100 // by allowing only `file` schemes 101 // unfortunately extensions that use diff views not always set this 102 // to something different than 'file' (see ongoing bug: #4608) 103 return document.languageId === "rust" && document.uri.scheme === "file"; 104} 105 106export function isCargoTomlDocument(document: vscode.TextDocument): document is RustDocument { 107 // ideally `document.languageId` should be 'toml' but user maybe not have toml extension installed 108 return document.uri.scheme === "file" && document.fileName.endsWith("Cargo.toml"); 109} 110 111export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 112 return isRustDocument(editor.document); 113} 114 115export function isDocumentInWorkspace(document: RustDocument): boolean { 116 const workspaceFolders = vscode.workspace.workspaceFolders; 117 if (!workspaceFolders) { 118 return false; 119 } 120 for (const folder of workspaceFolders) { 121 if (document.uri.fsPath.startsWith(folder.uri.fsPath)) { 122 return true; 123 } 124 } 125 return false; 126} 127 128export function isValidExecutable(path: string): boolean { 129 log.debug("Checking availability of a binary at", path); 130 131 const res = spawnSync(path, ["--version"], { encoding: "utf8" }); 132 133 const printOutput = res.error ? log.warn : log.info; 134 printOutput(path, "--version:", res); 135 136 return res.status === 0; 137} 138 139/** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */ 140export function setContextValue(key: string, value: any): Thenable<void> { 141 return vscode.commands.executeCommand("setContext", key, value); 142} 143 144/** 145 * Returns a higher-order function that caches the results of invoking the 146 * underlying async function. 147 */ 148export function memoizeAsync<Ret, TThis, Param extends string>( 149 func: (this: TThis, arg: Param) => Promise<Ret> 150) { 151 const cache = new Map<string, Ret>(); 152 153 return async function (this: TThis, arg: Param) { 154 const cached = cache.get(arg); 155 if (cached) return cached; 156 157 const result = await func.call(this, arg); 158 cache.set(arg, result); 159 160 return result; 161 }; 162} 163 164/** Awaitable wrapper around `child_process.exec` */ 165export function execute(command: string, options: ExecOptions): Promise<string> { 166 log.info(`running command: ${command}`); 167 return new Promise((resolve, reject) => { 168 exec(command, options, (err, stdout, stderr) => { 169 if (err) { 170 log.error(err); 171 reject(err); 172 return; 173 } 174 175 if (stderr) { 176 reject(new Error(stderr)); 177 return; 178 } 179 180 resolve(stdout.trimEnd()); 181 }); 182 }); 183} 184 185export function executeDiscoverProject(command: string, options: ExecOptions): Promise<string> { 186 log.info(`running command: ${command}`); 187 return new Promise((resolve, reject) => { 188 exec(command, options, (err, stdout, _) => { 189 if (err) { 190 log.error(err); 191 reject(err); 192 return; 193 } 194 195 resolve(stdout.trimEnd()); 196 }); 197 }); 198} 199 200export class LazyOutputChannel implements vscode.OutputChannel { 201 constructor(name: string) { 202 this.name = name; 203 } 204 205 name: string; 206 _channel: vscode.OutputChannel | undefined; 207 208 get channel(): vscode.OutputChannel { 209 if (!this._channel) { 210 this._channel = vscode.window.createOutputChannel(this.name); 211 } 212 return this._channel; 213 } 214 215 append(value: string): void { 216 this.channel.append(value); 217 } 218 appendLine(value: string): void { 219 this.channel.appendLine(value); 220 } 221 replace(value: string): void { 222 this.channel.replace(value); 223 } 224 clear(): void { 225 if (this._channel) { 226 this._channel.clear(); 227 } 228 } 229 show(preserveFocus?: boolean): void; 230 show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; 231 show(column?: any, preserveFocus?: any): void { 232 this.channel.show(column, preserveFocus); 233 } 234 hide(): void { 235 if (this._channel) { 236 this._channel.hide(); 237 } 238 } 239 dispose(): void { 240 if (this._channel) { 241 this._channel.dispose(); 242 } 243 } 244} 245