VSCodium/VS Code extension to support for Chez Scheme: Highlighting, autocompletion, documentation on hover and syntax checks.
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}