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

rokucommunity / brighterscript / #14044

20 Mar 2025 07:09PM UTC coverage: 87.163% (-2.0%) from 89.117%
#14044

push

web-flow
Merge e33b1f944 into 0eceb0830

13257 of 16072 branches covered (82.49%)

Branch coverage included in aggregate %.

1163 of 1279 new or added lines in 24 files covered. (90.93%)

802 existing lines in 52 files now uncovered.

14323 of 15570 relevant lines covered (91.99%)

21312.85 hits per line

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

79.28
/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 } from '../LspProject';
7
import { type LspProject } from '../LspProject';
8
import { isMainThread, parentPort } from 'worker_threads';
1✔
9
import { WorkerThreadProjectRunner } from './WorkerThreadProjectRunner';
1✔
10
import { WorkerPool } from './WorkerPool';
1✔
11
import type { Hover, MaybePromise, SemanticToken } from '../../interfaces';
12
import type { DocumentAction, DocumentActionWithStatus } from '../DocumentManager';
13
import { Deferred } from '../../deferred';
1✔
14
import type { FileTranspileResult, SignatureInfoObj } from '../../Program';
15
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList } from 'vscode-languageserver-protocol';
16
import type { Logger } from '../../logging';
17
import { createLogger } from '../../logging';
1✔
18
import * as fsExtra from 'fs-extra';
1✔
19

20
export const workerPool = new WorkerPool(() => {
1✔
21
    return new Worker(
1✔
22
        __filename,
23
        {
24
            //this needs to align with the same flag in the if statement below
25
            argv: ['--run-worker-thread-project-runner'],
26
            //wire up ts-node if we're running in ts-node
27
            execArgv: /\.ts$/i.test(__filename)
28
                ? ['--require', 'ts-node/register']
1✔
29
                /* istanbul ignore next */
30
                : undefined
31
        }
32
    );
33
});
34

35
//if this script is running in a Worker, start the project runner
36
/* istanbul ignore next */
37
if (!isMainThread && process.argv.includes('--run-worker-thread-project-runner')) {
38
    const runner = new WorkerThreadProjectRunner();
39
    runner.run(parentPort);
40
}
41

42
export class WorkerThreadProject implements LspProject {
1✔
43
    public constructor(
44
        options?: {
45
            logger?: Logger;
46
            projectIdentifier?: string;
47
        }
48
    ) {
49
        this.logger = options?.logger ?? createLogger();
4✔
50
        this.projectIdentifier = options?.projectIdentifier ?? '';
4✔
51
    }
52

53
    public async activate(options: ProjectConfig) {
54
        this.activateOptions = options;
4✔
55
        this.projectPath = options.projectPath ? util.standardizePath(options.projectPath) : options.projectPath;
4!
56
        this.workspaceFolder = options.workspaceFolder ? util.standardizePath(options.workspaceFolder) : options.workspaceFolder;
4✔
57
        this.projectNumber = options.projectNumber;
4✔
58
        this.bsconfigPath = options.bsconfigPath ? util.standardizePath(options.bsconfigPath) : options.bsconfigPath;
4!
59

60
        // start a new worker thread or get an unused existing thread
61
        this.worker = workerPool.getWorker();
4✔
62
        this.messageHandler = new MessageHandler<LspProject>({
4✔
63
            name: 'MainThread',
64
            port: this.worker,
65
            onRequest: this.processRequest.bind(this),
66
            onUpdate: this.processUpdate.bind(this)
67
        });
68
        this.disposables.push(this.messageHandler);
4✔
69

70
        const activateResponse = await this.messageHandler.sendRequest<ActivateResponse>('activate', { data: [options] });
4✔
71
        this.bsconfigPath = activateResponse.data.bsconfigPath;
4✔
72
        this.rootDir = activateResponse.data.rootDir;
4✔
73
        this.filePatterns = activateResponse.data.filePatterns;
4✔
74
        this.logger.logLevel = activateResponse.data.logLevel;
4✔
75

76
        //load the bsconfig file contents (used for performance optimizations externally)
77
        try {
4✔
78
            this.bsconfigFileContents = (await fsExtra.readFile(this.bsconfigPath)).toString();
4✔
79
        } catch { }
80

81

82
        this.activationDeferred.resolve();
4✔
83
        return activateResponse.data;
4✔
84
    }
85

86
    public logger: Logger;
87

88
    public isStandaloneProject = false;
4✔
89

90
    private activationDeferred = new Deferred();
4✔
91

92
    /**
93
     * Options used to activate this project
94
     */
95
    public activateOptions: ProjectConfig;
96

97
    /**
98
     * The root directory of the project
99
     */
100
    public rootDir: string;
101

102
    /**
103
     * The file patterns from bsconfig.json that were used to find all files for this project
104
     */
105
    public filePatterns: string[];
106

107
    /**
108
     * Path to a bsconfig.json file that will be used for this project
109
     */
110
    public bsconfigPath?: string;
111

112
    /**
113
     * 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).
114
     *
115
     * Only available after `.activate()` has completed.
116
     * @deprecated do not depend on this property. This will certainly be removed in a future release
117
     */
118
    bsconfigFileContents?: string;
119

120
    /**
121
     * The worker thread where the actual project will execute
122
     */
123
    private worker: Worker;
124

125
    /**
126
     * The path to where the project resides
127
     */
128
    public projectPath: string;
129

130
    /**
131
     * A unique number for this project, generated during this current language server session. Mostly used so we can identify which project is doing logging
132
     */
133
    public projectNumber: number;
134

135
    /**
136
     * A unique name for this project used in logs to help keep track of everything
137
     */
138
    public projectIdentifier: string;
139

140
    /**
141
     * The path to the workspace where this project resides. A workspace can have multiple projects (by adding a bsconfig.json to each folder).
142
     * Defaults to `.projectPath` if not set
143
     */
144
    public workspaceFolder: string;
145

146
    /**
147
     * Promise that resolves when the project finishes activating
148
     * @returns a promise that resolves when the project finishes activating
149
     */
150
    public whenActivated() {
151
        return this.activationDeferred.promise;
1✔
152
    }
153

154

155
    /**
156
     * Validate the project. This will trigger a full validation on any scopes that were changed since the last validation,
157
     * and will also eventually emit a new 'diagnostics' event that includes all diagnostics for the project
158
     */
159
    public async validate() {
NEW
160
        const response = await this.messageHandler.sendRequest<void>('validate');
×
NEW
161
        return response.data;
×
162
    }
163

164
    /**
165
     * Cancel any active validation that's running
166
     */
167
    public async cancelValidate() {
NEW
168
        const response = await this.messageHandler.sendRequest<void>('cancelValidate');
×
NEW
169
        return response.data;
×
170
    }
171

172
    public async getDiagnostics() {
173
        const response = await this.messageHandler.sendRequest<LspDiagnostic[]>('getDiagnostics');
1✔
174
        return response.data;
1✔
175
    }
176

177
    /**
178
     * 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
179
     * during the program's lifecycle flow
180
     */
181
    public async applyFileChanges(documentActions: DocumentAction[]): Promise<DocumentActionWithStatus[]> {
NEW
182
        const response = await this.messageHandler.sendRequest<DocumentActionWithStatus[]>('applyFileChanges', {
×
183
            data: [documentActions]
184
        });
NEW
185
        return response.data;
×
186
    }
187

188
    /**
189
     * Send a request with the standard structure
190
     * @param name the name of the request
191
     * @param data the array of data to send
192
     * @returns the response from the request
193
     */
194
    private async sendStandardRequest<T>(name: string, ...data: any[]) {
195
        const response = await this.messageHandler.sendRequest<T>(name as any, {
1✔
196
            data: data
197
        });
198
        return response.data;
1✔
199
    }
200

201
    /**
202
     * Get the full list of semantic tokens for the given file path
203
     */
204
    public async getSemanticTokens(options: { srcPath: string }) {
NEW
205
        return this.sendStandardRequest<SemanticToken[]>('getSemanticTokens', options);
×
206
    }
207

208
    public async transpileFile(options: { srcPath: string }) {
NEW
209
        return this.sendStandardRequest<FileTranspileResult>('transpileFile', options);
×
210
    }
211

212
    public async getHover(options: { srcPath: string; position: Position }): Promise<Hover[]> {
NEW
213
        return this.sendStandardRequest<Hover[]>('getHover', options);
×
214
    }
215

216
    public async getDefinition(options: { srcPath: string; position: Position }): Promise<Location[]> {
NEW
217
        return this.sendStandardRequest<Location[]>('getDefinition', options);
×
218
    }
219

220
    public async getSignatureHelp(options: { srcPath: string; position: Position }): Promise<SignatureInfoObj[]> {
NEW
221
        return this.sendStandardRequest<SignatureInfoObj[]>('getSignatureHelp', options);
×
222
    }
223

224
    public async getDocumentSymbol(options: { srcPath: string }): Promise<DocumentSymbol[]> {
NEW
225
        return this.sendStandardRequest<DocumentSymbol[]>('getDocumentSymbol', options);
×
226
    }
227

228
    public async getWorkspaceSymbol(): Promise<WorkspaceSymbol[]> {
229
        return this.sendStandardRequest<WorkspaceSymbol[]>('getWorkspaceSymbol');
1✔
230
    }
231

232
    public async getReferences(options: { srcPath: string; position: Position }): Promise<Location[]> {
NEW
233
        return this.sendStandardRequest<Location[]>('getReferences', options);
×
234
    }
235

236
    public async getCodeActions(options: { srcPath: string; range: Range }): Promise<CodeAction[]> {
NEW
237
        return this.sendStandardRequest<CodeAction[]>('getCodeActions', options);
×
238
    }
239

240
    public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
NEW
241
        return this.sendStandardRequest<CompletionList>('getCompletions', options);
×
242
    }
243

244
    /**
245
     * Handles request/response/update messages from the worker thread
246
     */
247
    private messageHandler: MessageHandler<LspProject>;
248

249
    private processRequest(request: WorkerMessage) {
250

251
    }
252

253
    private processUpdate(update: WorkerMessage) {
254
        //for now, all updates are treated like "events"
255
        this.emit(update.name as any, update.data);
12✔
256
    }
257

258
    public on(eventName: 'critical-failure', handler: (data: { message: string }) => void);
259
    public on(eventName: 'diagnostics', handler: (data: { diagnostics: LspDiagnostic[] }) => MaybePromise<void>);
260
    public on(eventName: 'all', handler: (eventName: string, data: any) => MaybePromise<void>);
261
    public on(eventName: string, handler: (...args: any[]) => MaybePromise<void>) {
262
        this.emitter.on(eventName, handler as any);
2✔
263
        return () => {
2✔
NEW
264
            this.emitter.removeListener(eventName, handler as any);
×
265
        };
266
    }
267

268
    private emit(eventName: 'critical-failure', data: { message: string });
269
    private emit(eventName: 'diagnostics', data: { diagnostics: LspDiagnostic[] });
270
    private async emit(eventName: string, data?) {
271
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
272
        await util.sleep(0);
12✔
273
        this.emitter.emit(eventName, data);
12✔
274
        //emit the 'all' event
275
        this.emitter.emit('all', eventName, data);
12✔
276
    }
277
    private emitter = new EventEmitter();
4✔
278

279
    public disposables: LspProject['disposables'] = [];
4✔
280

281
    public dispose() {
282
        for (let disposable of this.disposables ?? []) {
4!
283
            disposable?.dispose?.();
6!
284
        }
285
        this.disposables = [];
4✔
286

287
        //move the worker back to the pool so it can be used again
288
        if (this.worker) {
4!
289
            workerPool.releaseWorker(this.worker);
4✔
290
        }
291
        this.emitter?.removeAllListeners();
4!
292
    }
293
}
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