-
Notifications
You must be signed in to change notification settings - Fork 192
/
Copy pathcommon.ts
177 lines (152 loc) · 5.65 KB
/
common.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
import { exec, spawn as originalSpawn } from "child_process";
import { createHash } from "crypto";
import { promisify } from "util";
import * as vscode from "vscode";
import { State } from "vscode-languageclient";
import { Executable } from "vscode-languageclient/node";
export enum Command {
Start = "rubyLsp.start",
Stop = "rubyLsp.stop",
Restart = "rubyLsp.restart",
Update = "rubyLsp.update",
ToggleExperimentalFeatures = "rubyLsp.toggleExperimentalFeatures",
ServerOptions = "rubyLsp.serverOptions",
SelectVersionManager = "rubyLsp.selectRubyVersionManager",
ToggleFeatures = "rubyLsp.toggleFeatures",
FormatterHelp = "rubyLsp.formatterHelp",
RunTest = "rubyLsp.runTest",
RunTestInTerminal = "rubyLsp.runTestInTerminal",
DebugTest = "rubyLsp.debugTest",
ShowSyntaxTree = "rubyLsp.showSyntaxTree",
DisplayAddons = "rubyLsp.displayAddons",
RunTask = "rubyLsp.runTask",
BundleInstall = "rubyLsp.bundleInstall",
OpenFile = "rubyLsp.openFile",
FileOperation = "rubyLsp.fileOperation",
RailsGenerate = "rubyLsp.railsGenerate",
RailsDestroy = "rubyLsp.railsDestroy",
NewMinitestFile = "rubyLsp.newMinitestFile",
CollectRubyLspInfo = "rubyLsp.collectRubyLspInfo",
StartServerInDebugMode = "rubyLsp.startServerInDebugMode",
ShowOutput = "rubyLsp.showOutput",
MigrateLaunchConfiguration = "rubyLsp.migrateLaunchConfiguration",
}
export interface RubyInterface {
error: boolean;
versionManager: { identifier: string };
rubyVersion?: string;
}
export interface Addon {
name: string;
errored: boolean;
// Older versions of ruby-lsp don't return version for add-ons requests
version?: string;
}
export interface ClientInterface {
state: State;
formatter: string;
addons?: Addon[];
serverVersion?: string;
degraded: boolean;
sendRequest<T>(
method: string,
param: any,
token?: vscode.CancellationToken,
): Promise<T>;
}
export interface WorkspaceInterface {
ruby: RubyInterface;
lspClient?: ClientInterface;
error: boolean;
}
// Event emitter used to signal that the language status items need to be refreshed
export const STATUS_EMITTER = new vscode.EventEmitter<
WorkspaceInterface | undefined
>();
export const spawn = originalSpawn;
export const asyncExec = promisify(exec);
export const LSP_NAME = "Ruby LSP";
export const LOG_CHANNEL = vscode.window.createOutputChannel(LSP_NAME, {
log: true,
});
export const SUPPORTED_LANGUAGE_IDS = ["ruby", "erb"];
// A list of feature flags where the key is the name and the value is the rollout percentage.
//
// Note: names added here should also be added to the `rubyLsp.optedOutFeatureFlags` enum in the `package.json` file
export const FEATURE_FLAGS = {
tapiocaAddon: 0.0,
launcher: 0.05,
};
type FeatureFlagConfigurationKey = keyof typeof FEATURE_FLAGS | "all";
// Creates a debounced version of a function with the specified delay. If the function is invoked before the delay runs
// out, then the previous invocation of the function gets cancelled and a new one is scheduled.
//
// Example:
// ```typescript
// // Invoking debouncedFoo will only execute after a second has passed since the last of all invocations
// const debouncedFoo = debounce(this.foo.bind(this), 1000);
// ```
export function debounce(fn: (...args: any[]) => Promise<void>, delay: number) {
let timeoutID: NodeJS.Timeout | null = null;
return function (...args: any[]) {
if (timeoutID) {
clearTimeout(timeoutID);
}
return new Promise((resolve, reject) => {
timeoutID = setTimeout(() => {
fn(...args)
.then((result) => resolve(result))
.catch((error) => reject(error));
}, delay);
});
};
}
// Check if the given feature is enabled for the current user given the configured rollout percentage
export function featureEnabled(feature: keyof typeof FEATURE_FLAGS): boolean {
const flagConfiguration = vscode.workspace
.getConfiguration("rubyLsp")
.get<
Record<FeatureFlagConfigurationKey, boolean | undefined>
>("featureFlags")!;
// If the user opted out of this feature, return false. We explicitly check for `false` because `undefined` means
// nothing was configured
if (flagConfiguration[feature] === false || flagConfiguration.all === false) {
return false;
}
// If the user opted-in to all features, return true
if (flagConfiguration.all || flagConfiguration[feature]) {
return true;
}
const percentage = FEATURE_FLAGS[feature];
const machineId = vscode.env.machineId;
// Create a digest of the concatenated machine ID and feature name, which will generate a unique hash for this
// user-feature combination
const hash = createHash("sha256")
.update(`${machineId}-${feature}`)
.digest("hex");
// Convert the first 8 characters of the hash to a number between 0 and 1
const hashNum = parseInt(hash.substring(0, 8), 16) / 0xffffffff;
// If that number is below the percentage, then the feature is enabled for this user
return hashNum < percentage;
}
export function parseCommand(commandString: string): Executable {
// Regular expression to split arguments while respecting quotes
const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
const parts =
commandString.match(regex)?.map((arg) => {
// Remove surrounding quotes, if any
return arg.replace(/^['"]|['"]$/g, "");
}) ?? [];
// Extract environment variables
const env: Record<string, string> = {};
while (parts[0] && parts[0].includes("=")) {
const [key, value] = parts.shift()?.split("=") ?? [];
if (key) {
env[key] = value || "";
}
}
// The first part is the command, the rest are arguments
const command = parts.shift() || "";
const args = parts;
return { command, args, options: { env } };
}