VSCodium/VS Code extension to support for Chez Scheme: Highlighting, autocompletion, documentation on hover and syntax checks.
at main 334 lines 11 kB view raw
1/* 2 * SPDX-FileCopyrightText: Copyright 2023 Roland Csaszar 3 * SPDX-License-Identifier: MIT 4 * 5 * Project: vscode-scheme-repl 6 * File: helpers.ts 7 * Date: 18.May.2023 8 * 9 * ============================================================================== 10 * General helper functions. 11 */ 12 13/* eslint-disable camelcase */ 14 15import * as child_process from "child_process"; 16import * as vscode from "vscode"; 17import internal = require("stream"); 18 19/** 20 * The type of the extension's environment. 21 * That is state that is almost always needed. 22 */ 23export type Env = { 24 config: vscode.WorkspaceConfiguration; 25 outChannel: vscode.OutputChannel; 26 context: vscode.ExtensionContext; 27 diagnostics: vscode.DiagnosticCollection; 28 evalDecoration: vscode.TextEditorDecorationType; 29 evalDecorations: WeakMap<vscode.TextDocument, vscode.DecorationOptions[]>; 30 evalErrorDecoration: vscode.TextEditorDecorationType; 31 evalErrorDecorations: WeakMap< 32 vscode.TextDocument, 33 vscode.DecorationOptions[] 34 >; 35}; 36 37/** 38 * The `Maybe` type. Either `undefined`/`null` or a value. 39 */ 40export type Maybe<T> = T | undefined | null; 41 42/** 43 * The id function. 44 * Return the given argument `x`. 45 * @param x The argument to return. 46 * @returns The given argument `x`. 47 */ 48export function id<T>(x: T): T { 49 return x; 50} 51 52/** 53 * Return the last element of `a` or `undefined`, if `a` is an empty array. 54 * @param a The array to return the last element of. 55 * @returns The last element of `a` or `undefined`, if `a` is an empty array. 56 */ 57export function last<T>(a: T[]): T | undefined { 58 if (a.length === 0) { 59 return undefined; 60 } 61 return a[a.length - 1]; 62} 63 64/** 65 * Object holding the output of a process. 66 * 67 * Only two possible `Output`s exist: either 68 * `{ stdout: string; stderr: string }` or `{ error: string }`. 69 * 70 * - If an error occurred while executing the command, the field `error` is set to 71 * the message. Normally that means, that the command has not been found. 72 * `stdout` and `stderr` are both `undefined`. 73 * - If the command returned an error, the error message is returned in the 74 * field `stderr`, the output (if any) of stdout is in the string `stdout` and 75 * `error` is `undefined`. 76 * - If the command finished successfully, the output is returned in the field 77 * `stdout`, the field `stderr` should be the empty string `""` and `error` is 78 * `undefined`. 79 */ 80export type Output = { 81 stdout?: string; 82 stderr?: string; 83 error?: string; 84}; 85 86/** 87 * Do nothing for the given time `ms`. 88 * @param ms The sleep time in milliseconds. 89 */ 90export async function sleep(ms: number) { 91 return new Promise<void>((resolve) => { 92 setTimeout(resolve, ms); 93 }); 94} 95 96/** 97 * Regex to match all characters that need to be escaped when used in a 98 * `RegExp`. 99 */ 100const escapeRegex = /[.*+?^${}()|[\]\\]/gu; 101 102/** 103 * Regex to whitespace. 104 */ 105const whitespaceRegex = /[\s]+/gu; 106 107/** 108 * Regex that matches left parens, brackets or braces. 109 */ 110const parenRegexLeft = /[\]{<]|(?:\\\()/gu; 111 112/** 113 * Regex that matches right parens, brackets or braces. 114 */ 115const parenRegexRight = /[\]}>]|(?:\\\))/gu; 116 117/** 118 * Return the string `text` with all special characters escaped, for use in a 119 * `RegExp`. 120 * @param text The string to escape all special characters in. 121 * @returns The string `text` with all special characters escaped, for use in a 122 * `RegExp`. 123 */ 124export function escapeRegexp(text: string): string { 125 return text.replace(escapeRegex, "\\$&"); 126} 127 128/** 129 * Return the given string `text` with all potential places of whitespace 130 * replaced with a whitespace regex `\\s*`. 131 * @param text The string to process. 132 * @returns The given string `text` with all potential places of whitespace 133 * replaced with a whitespace regex `\\s*`. 134 */ 135export function makeWhitespaceGeneric(text: string): string { 136 return text 137 .replace(whitespaceRegex, "\\s+") 138 .replace(parenRegexLeft, "$&\\s*") 139 .replace(parenRegexRight, "\\s*$&"); 140} 141 142/** 143 * Return `def` if `s` if `undefined` or `null`, `s` else. 144 * @param s The object that can be either `undefined`/`null` or not. 145 * @param def The value to return if `s` is `undefined` or `null`. 146 * @returns `def` if `s` if `undefined` or `null`, `s` else. 147 */ 148export function fromMaybe<T>(s: Maybe<T>, def: T): T { 149 return s ? s : def; 150} 151 152/** 153 * Return the word (determined by the language's word borders) at `position` or 154 * `undefined`. 155 * @param document The text. 156 * @param position The position in the word to return. 157 * @returns The word (determined by the language's word borders) at `position` or 158 * `undefined`. 159 */ 160export function getWordAtPosition( 161 document: vscode.TextDocument, 162 position: vscode.Position 163): string | undefined { 164 const range = document.getWordRangeAtPosition(position); 165 return range ? document.getText(range) : undefined; 166} 167 168/** 169 * Return the root of the only workspace, the root of the workspace that the 170 * user selected or `undefined` if there is no currently open workspace 171 * (only a single file has been opened). 172 * @param askText The text to display if asking the user for a workspace. 173 * @returns The root of the only workspace, the root of the workspace that the 174 * user selected or `undefined` if there is no currently open workspace 175 * (only a single file has been opened). 176 */ 177export async function askForWorkspace( 178 askText: string 179): Promise<vscode.WorkspaceFolder | undefined> { 180 // eslint-disable-next-line no-eq-null, eqeqeq 181 if (vscode.workspace.workspaceFolders == null) { 182 return undefined; 183 } else if (vscode.workspace.workspaceFolders?.length === 1) { 184 return vscode.workspace.workspaceFolders[0]; 185 } else { 186 return vscode.window.showWorkspaceFolderPick({ 187 placeHolder: askText, 188 }); 189 } 190} 191 192/** 193 * Spawn the given command with the given arguments and return the output. 194 * Set `root` as the working directory of the command. 195 * `{ stdout; stderr; error }` is returned, see {@link Output}. 196 * @param data.root The current working directory for the command. 197 * @param data.cmd The command to call. 198 * @param data.args The arguments to pass to the command. 199 * @param data.input The string to send to the `stdin` of the process. 200 * @returns An object containing the output of the command's execution. 201 */ 202// eslint-disable-next-line max-statements 203export async function runCommand(data: { 204 root: string; 205 cmd: string; 206 args: string[]; 207 input: string; 208}): Promise<Output> { 209 const proc = child_process.spawn(data.cmd, data.args, { 210 cwd: data.root, 211 env: process.env, 212 }); 213 214 const checkCmd = new Promise((_, reject) => { 215 proc.on("error", reject); 216 }); 217 proc.stdin.write(data.input); 218 proc.stdin.end(); 219 220 const out = await readStream(proc.stdout); 221 const err = await readStream(proc.stderr); 222 223 const exitCode = new Promise<number>((resolve) => { 224 proc.on("close", resolve); 225 }); 226 227 try { 228 await Promise.race([checkCmd, exitCode]); 229 return { stdout: out, stderr: err }; 230 } catch (error) { 231 return { error: (error as Error).message }; 232 } 233} 234 235/** 236 * Return all data read from the given stream. 237 * @param stream The stream to read from. 238 * @returns All data read from the given stream. 239 */ 240export async function readStream(stream: internal.Readable): Promise<string> { 241 let out = ""; 242 for await (const chunk of stream) { 243 out = out.concat(chunk); 244 } 245 246 return out; 247} 248 249/** 250 * The color theme kind. Dark, light, high contrast light and high contrast 251 * dark. 252 * VS Code's type does not have an extra enum for "hc-light", and "hc-dark" is 253 * called `HighContrast`, because the light variant has been added later. 254 */ 255export type ColorThemeKind = "light" | "dark" | "hc-light" | "hc-dark"; 256 257/** 258 * Return the current color theme kind. 259 * That is, one of "light", "dark", "high contrast light" and "high contrast 260 * dark". 261 * @returns The current color theme kind. 262 */ 263export function getColorThemeKind(): ColorThemeKind { 264 switch (vscode.window.activeColorTheme.kind) { 265 case vscode.ColorThemeKind.Light: 266 return "light"; 267 case vscode.ColorThemeKind.Dark: 268 return "dark"; 269 case vscode.ColorThemeKind.HighContrast: 270 return "hc-dark"; 271 // ColorThemeKind === 4 272 default: 273 return "hc-light"; 274 } 275} 276 277/** 278 * Return a `Range` from `start` to `end`. 279 * @param start Either a `Position` or the tuple `[line, character]`. 280 * @param end Either a `Position` or the tuple `[line, character]`. 281 * @returns The `Range` from `start` to `end`. 282 */ 283export function rangeFromPositions( 284 start: vscode.Position | [number, number], 285 end: vscode.Position | [number, number] 286): vscode.Range { 287 const startPos = 288 start instanceof vscode.Position 289 ? start 290 : new vscode.Position(start[0], start[1]); 291 const endPos = 292 end instanceof vscode.Position 293 ? end 294 : new vscode.Position(end[0], end[1]); 295 return new vscode.Range(startPos, endPos); 296} 297 298/** 299 * Return the line and column of the character with index `charIndex` in `text`. 300 * Return `{ startLine: 0; startCol: 0 }` if something goes wrong, like an empty 301 * string for `text`. Lines and columns start at zero (`0`), not one (`1`). 302 * @param charIndex The index of the character in `text`. 303 * @param text The string to get the position of `charIndex` as line and column. 304 * @returns The line and column of the character with index `charIndex` in `text`. 305 */ 306export function getLineColFromCharIndex( 307 charIndex: number, 308 text: string 309): { startLine: number; startCol: number } { 310 const before = text.slice(0, charIndex); 311 const lastNewlineIdx = before.lastIndexOf("\n"); 312 const startCol = charIndex - (lastNewlineIdx < 0 ? 0 : lastNewlineIdx + 1); 313 const startLine = before.split("\n").length - 1; 314 315 return { startLine, startCol }; 316} 317 318/** 319 * Return the start position (line and column/character) of `end` in `text`. 320 * The prerequisite is that `end` does not end in whitespace, as whitespace is 321 * trimmed from `text`. 322 * @param text The whole string. 323 * @param end The substring at the end of `text`. 324 * @returns The start position (line and column/character) of `end` in `text`. 325 */ 326export function getStartPosition( 327 text: string, 328 end: string 329): { startLine: number; startCol: number } { 330 const trimmed = text.trimEnd(); 331 const whitespaceDiff = text.length - trimmed.length; 332 const idx = text.length - end.length - whitespaceDiff; 333 return getLineColFromCharIndex(idx, text); 334}