/* eslint suggest-no-throw/suggest-no-throw: 0 */ import * as vscode from 'vscode' import { strict as nativeAssert } from 'assert' import { exec, type ExecOptions, spawnSync } from 'child_process' import { inspect } from 'util' export interface Env { [name: string]: string } export function assert( condition: boolean, explanation: string ): asserts condition { try { nativeAssert(condition, explanation) } catch (err) { log.error(`Assertion failed:`, explanation) throw err } } class Logger { private enabled = true private readonly output = vscode.window.createOutputChannel( 'KittyCAD Language Client' ) setEnabled(yes: boolean): void { log.enabled = yes } // Hint: the type [T, ...T[]] means a non-empty array debug(...msg: [unknown, ...unknown[]]): void { if (!log.enabled) return log.write('DEBUG', ...msg) } info(...msg: [unknown, ...unknown[]]): void { log.write('INFO', ...msg) } warn(...msg: [unknown, ...unknown[]]): void { debugger log.write('WARN', ...msg) } error(...msg: [unknown, ...unknown[]]): void { debugger log.write('ERROR', ...msg) log.output.show(true) } private write(label: string, ...messageParts: unknown[]): void { const message = messageParts.map(log.stringify).join(' ') const dateTime = new Date().toLocaleString() log.output.appendLine(`${label} [${dateTime}]: ${message}`) } private stringify(val: unknown): string { if (typeof val === 'string') return val return inspect(val, { colors: false, depth: 6, // heuristic }) } } export const log = new Logger() export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } export type KclDocument = vscode.TextDocument & { languageId: 'kcl' } export type KclEditor = vscode.TextEditor & { document: KclDocument } export function isKclDocument( document: vscode.TextDocument ): document is KclDocument { // Prevent corrupted text (particularly via inlay hints) in diff views // by allowing only `file` schemes // unfortunately extensions that use diff views not always set this // to something different than 'file' (see ongoing bug: #4608) return document.languageId === 'kcl' && document.uri.scheme === 'file' } export function isCargoTomlDocument( document: vscode.TextDocument ): document is KclDocument { // ideally `document.languageId` should be 'toml' but user maybe not have toml extension installed return ( document.uri.scheme === 'file' && document.fileName.endsWith('Cargo.toml') ) } export function isKclEditor(editor: vscode.TextEditor): editor is KclEditor { return isKclDocument(editor.document) } export function isDocumentInWorkspace(document: KclDocument): boolean { const workspaceFolders = vscode.workspace.workspaceFolders if (!workspaceFolders) { return false } for (const folder of workspaceFolders) { if (document.uri.fsPath.startsWith(folder.uri.fsPath)) { return true } } return false } export function isValidExecutable(path: string): boolean { log.debug('Checking availability of a binary at', path) const res = spawnSync(path, ['--version'], { encoding: 'utf8', env: { ...process.env }, }) const printOutput = res.error ? log.warn : log.info printOutput(path, '--version:', res) return res.status === 0 } /** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */ export function setContextValue(key: string, value: any): Thenable { return vscode.commands.executeCommand('setContext', key, value) } /** * Returns a higher-order function that caches the results of invoking the * underlying async function. */ export function memoizeAsync( func: (this: TThis, arg: Param) => Promise ) { const cache = new Map() return async function (this: TThis, arg: Param) { const cached = cache.get(arg) if (cached) return cached const result = await func.call(this, arg) cache.set(arg, result) return result } } /** Awaitable wrapper around `child_process.exec` */ export function execute( command: string, options: ExecOptions ): Promise { log.info(`running command: ${command}`) return new Promise((resolve, reject) => { exec(command, options, (err, stdout, stderr) => { if (err) { log.error(err) reject(err) return } if (stderr) { reject(new Error(stderr)) return } resolve(stdout.trimEnd()) }) }) } export function executeDiscoverProject( command: string, options: ExecOptions ): Promise { options = Object.assign({ maxBuffer: 10 * 1024 * 1024 }, options) log.info(`running command: ${command}`) return new Promise((resolve, reject) => { exec(command, options, (err, stdout, _) => { if (err) { log.error(err) reject(err) return } resolve(stdout.trimEnd()) }) }) } export class LazyOutputChannel implements vscode.OutputChannel { constructor(name: string) { this.name = name } name: string _channel: vscode.OutputChannel | undefined get channel(): vscode.OutputChannel { if (!this._channel) { this._channel = vscode.window.createOutputChannel(this.name) } return this._channel } append(value: string): void { this.channel.append(value) } appendLine(value: string): void { this.channel.appendLine(value) } replace(value: string): void { this.channel.replace(value) } clear(): void { if (this._channel) { this._channel.clear() } } show(preserveFocus?: boolean): void show(column?: vscode.ViewColumn, preserveFocus?: boolean): void show(column?: any, preserveFocus?: any): void { this.channel.show(column, preserveFocus) } hide(): void { if (this._channel) { this._channel.hide() } } dispose(): void { if (this._channel) { this._channel.dispose() } } }