-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathWorkerThreadProject.ts
More file actions
300 lines (248 loc) · 11.4 KB
/
WorkerThreadProject.ts
File metadata and controls
300 lines (248 loc) · 11.4 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
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
import * as EventEmitter from 'eventemitter3';
import { Worker } from 'worker_threads';
import type { WorkerMessage } from './MessageHandler';
import { MessageHandler } from './MessageHandler';
import util from '../../util';
import type { LspDiagnostic, ActivateResponse, ProjectConfig } from '../LspProject';
import { type LspProject } from '../LspProject';
import { WorkerPool } from './WorkerPool';
import type { Hover, MaybePromise, SemanticToken } from '../../interfaces';
import type { DocumentAction, DocumentActionWithStatus } from '../DocumentManager';
import { Deferred } from '../../deferred';
import type { FileTranspileResult, SignatureInfoObj } from '../../Program';
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList } from 'vscode-languageserver-protocol';
import type { Logger } from '../../logging';
import { createLogger } from '../../logging';
import * as fsExtra from 'fs-extra';
import * as path from 'path';
import { standardizePath as s } from '../../util';
export const workerPool = new WorkerPool(() => {
//construct the path to the `./run.ts` (or `./run.js`) script in this same directory
const runScriptPath = s`${__dirname}/run${path.extname(__filename)}`;
// Prepare execArgv for debugging support
const execArgv: string[] = [];
// Add ts-node if we're running TypeScript
if (/\.ts$/i.test(runScriptPath)) {
execArgv.push('--require', 'ts-node/register');
}
// Enable debugging for worker threads if the main process is being debugged
// Check if debugging is enabled via execArgv or environment variables
const isDebugging = process.execArgv.some(arg => arg.startsWith('--inspect')) || process.env.NODE_OPTIONS?.includes('--inspect');
if (isDebugging) {
// Node.js will automatically assign a unique port for each worker when using --inspect=0
// This allows VSCode to automatically attach to worker threads
execArgv.push('--inspect=0');
}
return new Worker(
runScriptPath,
{
execArgv: execArgv.length > 0 ? execArgv : undefined
}
);
});
export class WorkerThreadProject implements LspProject {
public constructor(
options?: {
logger?: Logger;
}
) {
this.logger = options?.logger ?? createLogger();
}
public async activate(options: ProjectConfig) {
this.activateOptions = options;
this.bsconfigPath = options.bsconfigPath ? util.standardizePath(options.bsconfigPath) : options.bsconfigPath;
this.projectDir = options.projectDir ? util.standardizePath(options.projectDir) : options.projectDir;
this.projectKey = options.projectKey ? util.standardizePath(options.projectKey) : options.bsconfigPath ?? options.projectDir;
this.workspaceFolder = options.workspaceFolder ? util.standardizePath(options.workspaceFolder) : options.workspaceFolder;
this.projectNumber = options.projectNumber;
// start a new worker thread or get an unused existing thread
this.worker = workerPool.getWorker();
this.messageHandler = new MessageHandler<LspProject>({
name: 'MainThread',
port: this.worker,
onRequest: this.processRequest.bind(this),
onUpdate: this.processUpdate.bind(this)
});
this.disposables.push(this.messageHandler);
const activateResponse = await this.messageHandler.sendRequest<ActivateResponse>('activate', { data: [options] });
this.bsconfigPath = activateResponse.data.bsconfigPath;
this.rootDir = activateResponse.data.rootDir;
this.filePatterns = activateResponse.data.filePatterns;
this.logger.logLevel = activateResponse.data.logLevel;
//load the bsconfig file contents (used for performance optimizations externally)
try {
this.bsconfigFileContents = (await fsExtra.readFile(this.bsconfigPath)).toString();
} catch { }
this.activationDeferred.resolve();
return activateResponse.data;
}
public logger: Logger;
public isStandaloneProject = false;
private activationDeferred = new Deferred();
/**
* Options used to activate this project
*/
public activateOptions: ProjectConfig;
/**
* The root directory of the project
*/
public rootDir: string;
/**
* The file patterns from bsconfig.json that were used to find all files for this project
*/
public filePatterns: string[];
/**
* Path to a bsconfig.json file that will be used for this project
*/
public bsconfigPath?: string;
/**
* The contents of the bsconfig.json file. This is used to detect when the bsconfig file has not actually been changed (even if the fs says it did).
*
* Only available after `.activate()` has completed.
* @deprecated do not depend on this property. This will certainly be removed in a future release
*/
bsconfigFileContents?: string;
/**
* The worker thread where the actual project will execute
*/
private worker: Worker;
/**
* Path to the project. For directory-only projects, this is the path to the dir. For bsconfig.json projects, this is the path to the config.
*/
projectKey: string;
/**
* The directory for the root of this project (typically where the bsconfig.json or manifest is located)
*/
projectDir: string;
/**
* A unique number for this project, generated during this current language server session. Mostly used so we can identify which project is doing logging
*/
public projectNumber: number;
/**
* The path to the workspace where this project resides. A workspace can have multiple projects (by adding a bsconfig.json to each folder).
* Defaults to `.projectPath` if not set
*/
public workspaceFolder: string;
/**
* Promise that resolves when the project finishes activating
* @returns a promise that resolves when the project finishes activating
*/
public whenActivated() {
return this.activationDeferred.promise;
}
/**
* Validate the project. This will trigger a full validation on any scopes that were changed since the last validation,
* and will also eventually emit a new 'diagnostics' event that includes all diagnostics for the project
*/
public async validate() {
const response = await this.messageHandler.sendRequest<void>('validate');
return response.data;
}
/**
* Cancel any active validation that's running
*/
public async cancelValidate() {
const response = await this.messageHandler.sendRequest<void>('cancelValidate');
return response.data;
}
public async getDiagnostics() {
const response = await this.messageHandler.sendRequest<LspDiagnostic[]>('getDiagnostics');
return response.data;
}
/**
* Apply a series of file changes to the project. This is safe to call any time. Changes will be queued and flushed at the correct times
* during the program's lifecycle flow
*/
public async applyFileChanges(documentActions: DocumentAction[]): Promise<DocumentActionWithStatus[]> {
const response = await this.messageHandler.sendRequest<DocumentActionWithStatus[]>('applyFileChanges', {
data: [documentActions]
});
return response.data;
}
/**
* Send a request with the standard structure
* @param name the name of the request
* @param data the array of data to send
* @returns the response from the request
*/
private async sendStandardRequest<T>(name: string, ...data: any[]) {
const response = await this.messageHandler.sendRequest<T>(name as any, {
data: data
});
return response.data;
}
/**
* Get the full list of semantic tokens for the given file path
*/
public async getSemanticTokens(options: { srcPath: string }) {
return this.sendStandardRequest<SemanticToken[]>('getSemanticTokens', options);
}
public async transpileFile(options: { srcPath: string }) {
return this.sendStandardRequest<FileTranspileResult>('transpileFile', options);
}
public async getHover(options: { srcPath: string; position: Position }): Promise<Hover[]> {
return this.sendStandardRequest<Hover[]>('getHover', options);
}
public async getDefinition(options: { srcPath: string; position: Position }): Promise<Location[]> {
return this.sendStandardRequest<Location[]>('getDefinition', options);
}
public async getSignatureHelp(options: { srcPath: string; position: Position }): Promise<SignatureInfoObj[]> {
return this.sendStandardRequest<SignatureInfoObj[]>('getSignatureHelp', options);
}
public async getDocumentSymbol(options: { srcPath: string }): Promise<DocumentSymbol[]> {
return this.sendStandardRequest<DocumentSymbol[]>('getDocumentSymbol', options);
}
public async getWorkspaceSymbol(): Promise<WorkspaceSymbol[]> {
return this.sendStandardRequest<WorkspaceSymbol[]>('getWorkspaceSymbol');
}
public async getReferences(options: { srcPath: string; position: Position }): Promise<Location[]> {
return this.sendStandardRequest<Location[]>('getReferences', options);
}
public async getCodeActions(options: { srcPath: string; range: Range }): Promise<CodeAction[]> {
return this.sendStandardRequest<CodeAction[]>('getCodeActions', options);
}
public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
return this.sendStandardRequest<CompletionList>('getCompletions', options);
}
/**
* Handles request/response/update messages from the worker thread
*/
private messageHandler: MessageHandler<LspProject>;
private processRequest(request: WorkerMessage) {
}
private processUpdate(update: WorkerMessage) {
//for now, all updates are treated like "events"
this.emit(update.name as any, update.data);
}
public on(eventName: 'critical-failure', handler: (data: { message: string }) => void);
public on(eventName: 'diagnostics', handler: (data: { diagnostics: LspDiagnostic[] }) => MaybePromise<void>);
public on(eventName: 'all', handler: (eventName: string, data: any) => MaybePromise<void>);
public on(eventName: string, handler: (...args: any[]) => MaybePromise<void>) {
this.emitter.on(eventName, handler as any);
return () => {
this.emitter.removeListener(eventName, handler as any);
};
}
private emit(eventName: 'critical-failure', data: { message: string });
private emit(eventName: 'diagnostics', data: { diagnostics: LspDiagnostic[] });
private async emit(eventName: string, data?) {
//emit these events on next tick, otherwise they will be processed immediately which could cause issues
await util.sleep(0);
this.emitter.emit(eventName, data);
//emit the 'all' event
this.emitter.emit('all', eventName, data);
}
private emitter = new EventEmitter();
public disposables: LspProject['disposables'] = [];
public dispose() {
for (let disposable of this.disposables ?? []) {
disposable?.dispose?.();
}
this.disposables = [];
//move the worker back to the pool so it can be used again
if (this.worker) {
workerPool.releaseWorker(this.worker);
}
this.emitter?.removeAllListeners();
}
}