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