forked from microsoft/vscode-cpptools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexpand.ts
More file actions
139 lines (121 loc) · 5.74 KB
/
Copy pathexpand.ts
File metadata and controls
139 lines (121 loc) · 5.74 KB
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
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
/* eslint-disable no-cond-assign */
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { isString, replaceAll } from './common';
import { getOutputChannelLogger } from './logger';
/**
* Support ExpansionVars (${var}), env (${env:var}), and optionally VS CODE commands (${command:commandID}).
* Supported format follows https://code.visualstudio.com/docs/editor/variables-reference
* Expand options and functions are mofidifed from https://github.com/microsoft/vscode-cmake-tools/blob/main/src/expand.ts
*/
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
export interface ExpansionVars {
[key: string]: string;
workspaceFolder: string;
workspaceFolderBasename: string;
}
export interface ExpansionOptions {
vars: ExpansionVars;
doNotSupportCommands?: boolean;
recursive?: boolean;
}
export async function expandAllStrings(obj: any, options: ExpansionOptions): Promise<void> {
if (Array.isArray(obj) || (obj !== null && typeof obj === 'object')) {
for (const key of Object.keys(obj)) {
if (isString(obj[key])) {
obj[key] = await expandString(obj[key], options);
} else {
await expandAllStrings(obj[key], options);
}
}
}
}
export async function expandString(input: string, options: ExpansionOptions): Promise<string> {
const MAX_RECURSION: number = 10;
let result: string = input;
let didReplacement: boolean = false;
let i: number = 0;
do {
// TODO: consider a full circular reference check?
[result, didReplacement] = await expandStringImpl(result, options);
i++;
} while (i < MAX_RECURSION && options.recursive && didReplacement);
if (i === MAX_RECURSION && didReplacement) {
void getOutputChannelLogger().showErrorMessage(localize('max.recursion.reached', 'Reached max string expansion recursion. Possible circular reference.'));
}
return replaceAll(result, '${dollar}', '$');
}
/** Returns [expandedString, didReplacement] */
async function expandStringImpl(input: string, options: ExpansionOptions): Promise<[string, boolean]> {
if (!input) {
return [input, false];
}
// We accumulate a list of substitutions that we need to make, preventing
// recursively expanding or looping forever on bad replacements
const subs: Map<string, string> = new Map<string, string>();
const var_re: RegExp = /\$\{(\w+)\}/g;
let match: RegExpMatchArray | null = null;
while (match = var_re.exec(input)) {
const full: string = match[0];
const key: string = match[1];
if (key !== 'dollar') {
// Replace dollar sign at the very end of the expanding process.
// Distinguish "variable not defined" (undefined) from "variable defined
// as empty string". A defined-but-empty value (e.g. workspaceFolderBasename
// when the workspace root is "/" or "C:\\") should substitute as "" rather
// than be reported as an invalid reference.
const repl: string | undefined = options.vars[key];
if (repl === undefined) {
void getOutputChannelLogger().showWarningMessage(localize('invalid.var.reference', 'Invalid variable reference {0} in string: {1}.', full, input));
} else {
subs.set(full, repl);
}
}
}
// Regular expression for variable value (between the variable suffix and the next ending curly bracket):
// .+? matches any character (except line terminators) between one and unlimited times,
// as few times as possible, expanding as needed (lazy)
const varValueRegexp: string = ".+?";
const env_re: RegExp = RegExp(`\\$\\{env:(${varValueRegexp})\\}`, "g");
while (match = env_re.exec(input)) {
const full: string = match[0];
const varname: string = match[1];
if (process.env[varname] === undefined) {
void getOutputChannelLogger().showWarningMessage(localize('env.var.not.found', 'Environment variable {0} not found', varname));
}
const repl: string = process.env[varname] || '';
subs.set(full, repl);
}
const command_re: RegExp = RegExp(`\\$\\{command:(${varValueRegexp})\\}`, "g");
while (match = command_re.exec(input)) {
if (options.doNotSupportCommands) {
void getOutputChannelLogger().showWarningMessage(localize('commands.not.supported', 'Commands are not supported for string: {0}.', input));
break;
}
const full: string = match[0];
const command: string = match[1];
if (subs.has(full)) {
continue; // Don't execute commands more than once per string
}
try {
const command_ret: unknown = await vscode.commands.executeCommand(command, options.vars.workspaceFolder);
subs.set(full, `${command_ret}`);
} catch (e: any) {
void getOutputChannelLogger().showWarningMessage(localize('exception.executing.command', 'Exception while executing command {0} for string: {1} {2}.', command, input, e));
}
}
let result: string = input;
let didReplacement: boolean = false;
subs.forEach((value, key) => {
if (value !== key) {
result = replaceAll(result, key, value);
didReplacement = true;
}
});
return [result, didReplacement];
}