• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / 26661434192

29 May 2026 08:48PM UTC coverage: 87.054%. First build
26661434192

Pull #1726

github

web-flow
Merge 883a1db73 into 3ab3aaf0c
Pull Request #1726: Merge master into v1

15999 of 19398 branches covered (82.48%)

Branch coverage included in aggregate %.

74 of 122 new or added lines in 7 files covered. (60.66%)

16660 of 18118 relevant lines covered (91.95%)

27556.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.36
/src/lsp/worker/WorkerThreadProject.ts
1
import * as EventEmitter from 'eventemitter3';
1✔
2
import { Worker } from 'worker_threads';
1✔
3
import type { WorkerMessage } from './MessageHandler';
4
import { MessageHandler } from './MessageHandler';
1✔
5
import util from '../../util';
1✔
6
import type { LspDiagnostic, ActivateResponse, ProjectConfig, FileRenameTextEdit } from '../LspProject';
7
import { type LspProject } from '../LspProject';
8
import { WorkerPool } from './WorkerPool';
1✔
9
import type { Hover, MaybePromise, SemanticToken } from '../../interfaces';
10
import type { DocumentAction, DocumentActionWithStatus } from '../DocumentManager';
11
import { Deferred } from '../../deferred';
1✔
12
import type { FileTranspileResult, SignatureInfoObj } from '../../Program';
13
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList, SelectionRange, InlayHint } from 'vscode-languageserver-protocol';
14
import type { Logger } from '../../logging';
15
import { createLogger } from '../../logging';
1✔
16
import * as fsExtra from 'fs-extra';
1✔
17
import * as path from 'path';
1✔
18
import { standardizePath as s } from '../../util';
1✔
19

20

21
export const workerPool = new WorkerPool(() => {
1✔
22
    //construct the path to the `./run.ts` (or `./run.js`) script in this same directory
23
    const runScriptPath = s`${__dirname}/run${path.extname(__filename)}`;
1✔
24

25
    // Prepare execArgv for debugging support
26
    const execArgv: string[] = [];
1✔
27

28
    // Add ts-node if we're running TypeScript
29
    if (/\.ts$/i.test(runScriptPath)) {
1!
30
        execArgv.push('--require', 'ts-node/register');
1✔
31
    }
32

33
    // Enable debugging for worker threads if the main process is being debugged
34
    // Check if debugging is enabled via execArgv or environment variables
35
    const isDebugging = process.execArgv.some(arg => arg.startsWith('--inspect')) || process.env.NODE_OPTIONS?.includes('--inspect');
1!
36

37
    if (isDebugging) {
1!
38
        // Node.js will automatically assign a unique port for each worker when using --inspect=0
39
        // This allows VSCode to automatically attach to worker threads
40
        execArgv.push('--inspect=0');
×
41
    }
42

43
    return new Worker(
1✔
44
        runScriptPath,
45
        {
46
            execArgv: execArgv.length > 0 ? execArgv : undefined
1!
47
        }
48
    );
49
});
50

51
export class WorkerThreadProject implements LspProject {
1✔
52
    public constructor(
53
        options?: {
54
            logger?: Logger;
55
        }
56
    ) {
57
        this.logger = options?.logger ?? createLogger();
4✔
58
    }
59

60
    public async activate(options: ProjectConfig) {
61
        this.activateOptions = options;
4✔
62
        this.bsconfigPath = options.bsconfigPath ? util.standardizePath(options.bsconfigPath) : options.bsconfigPath;
4✔
63
        this.projectDir = options.projectDir ? util.standardizePath(options.projectDir) : options.projectDir;
4✔
64
        this.projectKey = options.projectKey ? util.standardizePath(options.projectKey) : options.bsconfigPath ?? options.projectDir;
4!
65
        this.workspaceFolder = options.workspaceFolder ? util.standardizePath(options.workspaceFolder) : options.workspaceFolder;
4✔
66
        this.projectNumber = options.projectNumber;
4✔
67

68
        // start a new worker thread or get an unused existing thread
69
        this.worker = workerPool.getWorker();
4✔
70
        this.messageHandler = new MessageHandler<LspProject>({
4✔
71
            name: 'MainThread',
72
            port: this.worker,
73
            onRequest: this.processRequest.bind(this),
74
            onUpdate: this.processUpdate.bind(this)
75
        });
76
        this.disposables.push(this.messageHandler);
4✔
77

78
        const activateResponse = await this.messageHandler.sendRequest<ActivateResponse>('activate', { data: [options] });
4✔
79
        this.bsconfigPath = activateResponse.data.bsconfigPath;
4✔
80
        this.rootDir = activateResponse.data.rootDir;
4✔
81
        this.filePatterns = activateResponse.data.filePatterns;
4✔
82
        this.logger.logLevel = activateResponse.data.logLevel;
4✔
83
        this.manifestSrcPath = activateResponse.data.manifestSrcPath;
4✔
84

85
        //load the bsconfig file contents (used for performance optimizations externally)
86
        try {
4✔
87
            this.bsconfigFileContents = (await fsExtra.readFile(this.bsconfigPath)).toString();
4✔
88
        } catch { }
89

90
        //load the manifest file contents (used for change detection to trigger project reloads).
91
        //use manifestSrcPath from the activation response which reflects the actual src path (respects {src;dest} mappings)
92
        try {
4✔
93
            this.manifestFileContents = (await fsExtra.readFile(this.manifestSrcPath)).toString();
4✔
94
        } catch { }
95

96

97
        this.activationDeferred.resolve();
4✔
98
        return activateResponse.data;
4✔
99
    }
100

101
    public logger: Logger;
102

103
    public isStandaloneProject = false;
4✔
104

105
    private activationDeferred = new Deferred();
4✔
106

107
    /**
108
     * Options used to activate this project
109
     */
110
    public activateOptions: ProjectConfig;
111

112
    /**
113
     * The root directory of the project
114
     */
115
    public rootDir: string;
116

117
    /**
118
     * The file patterns from bsconfig.json that were used to find all files for this project
119
     */
120
    public filePatterns: string[];
121

122
    /**
123
     * Path to a bsconfig.json file that will be used for this project
124
     */
125
    public bsconfigPath?: string;
126

127
    /**
128
     * 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).
129
     *
130
     * Only available after `.activate()` has completed.
131
     * @deprecated do not depend on this property. This will certainly be removed in a future release
132
     */
133
    bsconfigFileContents?: string;
134

135
    /**
136
     * The contents of the manifest file. This is used to detect when the manifest file has not actually been changed (even if the fs says it did).
137
     *
138
     * Only available after `.activate()` has completed.
139
     * @deprecated do not depend on this property. This will certainly be removed in a future release
140
     */
141
    manifestFileContents?: string;
142

143
    /**
144
     * The absolute source path to the manifest file (the file that maps to dest 'manifest').
145
     * May differ from rootDir/manifest if the project uses a custom {src; dest} mapping.
146
     *
147
     * Only available after `.activate()` has completed.
148
     */
149
    manifestSrcPath?: string;
150

151
    /**
152
     * The worker thread where the actual project will execute
153
     */
154
    private worker: Worker;
155

156
    /**
157
     * 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.
158
     */
159
    projectKey: string;
160
    /**
161
     * The directory for the root of this project (typically where the bsconfig.json or manifest is located)
162
     */
163
    projectDir: string;
164

165
    /**
166
     * A unique number for this project, generated during this current language server session. Mostly used so we can identify which project is doing logging
167
     */
168
    public projectNumber: number;
169

170
    /**
171
     * The path to the workspace where this project resides. A workspace can have multiple projects (by adding a bsconfig.json to each folder).
172
     * Defaults to `.projectPath` if not set
173
     */
174
    public workspaceFolder: string;
175

176
    /**
177
     * Promise that resolves when the project finishes activating
178
     * @returns a promise that resolves when the project finishes activating
179
     */
180
    public whenActivated() {
181
        return this.activationDeferred.promise;
1✔
182
    }
183

184

185
    /**
186
     * Validate the project. This will trigger a full validation on any scopes that were changed since the last validation,
187
     * and will also eventually emit a new 'diagnostics' event that includes all diagnostics for the project
188
     */
189
    public async validate() {
190
        const response = await this.messageHandler.sendRequest<void>('validate');
3✔
191
        return response.data;
2✔
192
    }
193

194
    /**
195
     * Cancel any active validation that's running
196
     */
197
    public async cancelValidate() {
198
        const response = await this.messageHandler.sendRequest<void>('cancelValidate');
×
199
        return response.data;
×
200
    }
201

202
    public async getDiagnostics() {
203
        const response = await this.messageHandler.sendRequest<LspDiagnostic[]>('getDiagnostics');
1✔
204
        return response.data;
1✔
205
    }
206

207
    /**
208
     * 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
209
     * during the program's lifecycle flow
210
     */
211
    public async applyFileChanges(documentActions: DocumentAction[]): Promise<DocumentActionWithStatus[]> {
212
        const response = await this.messageHandler.sendRequest<DocumentActionWithStatus[]>('applyFileChanges', {
×
213
            data: [documentActions]
214
        });
215
        return response.data;
×
216
    }
217

218
    /**
219
     * Send a request with the standard structure
220
     * @param name the name of the request
221
     * @param data the array of data to send
222
     * @returns the response from the request
223
     */
224
    private async sendStandardRequest<T>(name: string, ...data: any[]) {
225
        const response = await this.messageHandler.sendRequest<T>(name as any, {
1✔
226
            data: data
227
        });
228
        return response.data;
1✔
229
    }
230

231
    /**
232
     * Get the full list of semantic tokens for the given file path
233
     */
234
    public async getSemanticTokens(options: { srcPath: string }) {
235
        return this.sendStandardRequest<SemanticToken[]>('getSemanticTokens', options);
×
236
    }
237

238
    public async transpileFile(options: { srcPath: string }) {
239
        return this.sendStandardRequest<FileTranspileResult>('transpileFile', options);
×
240
    }
241

242
    public async getHover(options: { srcPath: string; position: Position }): Promise<Hover[]> {
243
        return this.sendStandardRequest<Hover[]>('getHover', options);
×
244
    }
245

246
    public async getDefinition(options: { srcPath: string; position: Position }): Promise<Location[]> {
247
        return this.sendStandardRequest<Location[]>('getDefinition', options);
×
248
    }
249

250
    public async getSignatureHelp(options: { srcPath: string; position: Position }): Promise<SignatureInfoObj[]> {
251
        return this.sendStandardRequest<SignatureInfoObj[]>('getSignatureHelp', options);
×
252
    }
253

254
    public async getDocumentSymbol(options: { srcPath: string }): Promise<DocumentSymbol[]> {
255
        return this.sendStandardRequest<DocumentSymbol[]>('getDocumentSymbol', options);
×
256
    }
257

258
    public async getWorkspaceSymbol(): Promise<WorkspaceSymbol[]> {
259
        return this.sendStandardRequest<WorkspaceSymbol[]>('getWorkspaceSymbol');
1✔
260
    }
261

262
    public async getReferences(options: { srcPath: string; position: Position }): Promise<Location[]> {
263
        return this.sendStandardRequest<Location[]>('getReferences', options);
×
264
    }
265

266
    public async getFileRenameEdits(options: { oldSrcPath: string; newSrcPath: string }): Promise<FileRenameTextEdit[]> {
267
        return this.sendStandardRequest<FileRenameTextEdit[]>('getFileRenameEdits', options);
×
268
    }
269

270
    public async getCodeActions(options: { srcPath: string; range: Range }): Promise<CodeAction[]> {
271
        return this.sendStandardRequest<CodeAction[]>('getCodeActions', options);
×
272
    }
273

274
    public async getSourceFixAllCodeActions(options: { srcPath: string }): Promise<CodeAction[]> {
275
        return this.sendStandardRequest<CodeAction[]>('getSourceFixAllCodeActions', options);
×
276
    }
277

278
    public async getSelectionRanges(options: { srcPath: string; positions: Position[] }): Promise<SelectionRange[]> {
279
        return this.sendStandardRequest<SelectionRange[]>('getSelectionRanges', options);
×
280
    }
281

282
    public async getInlayHints(options: { srcPath: string; range: Range }): Promise<InlayHint[]> {
NEW
283
        return this.sendStandardRequest<InlayHint[]>('getInlayHints', options);
×
284
    }
285

286
    public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
287
        return this.sendStandardRequest<CompletionList>('getCompletions', options);
×
288
    }
289

290
    /**
291
     * Handles request/response/update messages from the worker thread
292
     */
293
    private messageHandler: MessageHandler<LspProject>;
294

295
    private processRequest(request: WorkerMessage) {
296

297
    }
298

299
    private processUpdate(update: WorkerMessage) {
300
        //for now, all updates are treated like "events"
301
        this.emit(update.name as any, update.data);
15✔
302
    }
303

304
    public on(eventName: 'critical-failure', handler: (data: { message: string }) => void);
305
    public on(eventName: 'diagnostics', handler: (data: { diagnostics: LspDiagnostic[] }) => MaybePromise<void>);
306
    public on(eventName: 'all', handler: (eventName: string, data: any) => MaybePromise<void>);
307
    public on(eventName: string, handler: (...args: any[]) => MaybePromise<void>) {
308
        this.emitter.on(eventName, handler as any);
2✔
309
        return () => {
2✔
310
            this.emitter.removeListener(eventName, handler as any);
×
311
        };
312
    }
313

314
    private emit(eventName: 'critical-failure', data: { message: string });
315
    private emit(eventName: 'diagnostics', data: { diagnostics: LspDiagnostic[] });
316
    private async emit(eventName: string, data?) {
317
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
318
        await util.sleep(0);
15✔
319
        this.emitter.emit(eventName, data);
15✔
320
        //emit the 'all' event
321
        this.emitter.emit('all', eventName, data);
15✔
322
    }
323
    private emitter = new EventEmitter();
4✔
324

325
    public disposables: LspProject['disposables'] = [];
4✔
326

327
    public dispose() {
328
        for (let disposable of this.disposables ?? []) {
4!
329
            disposable?.dispose?.();
6!
330
        }
331
        this.disposables = [];
4✔
332

333
        //move the worker back to the pool so it can be used again
334
        if (this.worker) {
4!
335
            workerPool.releaseWorker(this.worker);
4✔
336
        }
337
        this.emitter?.removeAllListeners();
4!
338
    }
339
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc