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

rokucommunity / vscode-brightscript-language / 26237148523

21 May 2026 03:53PM UTC coverage: 55.549% (+0.05%) from 55.501%
26237148523

Pull #802

github

web-flow
Merge 54da9e620 into b9f6aae1a
Pull Request #802: Route extension logs through the BrightScript Extension output channel

2196 of 4384 branches covered (50.09%)

Branch coverage included in aggregate %.

18 of 22 new or added lines in 3 files covered. (81.82%)

3535 of 5933 relevant lines covered (59.58%)

39.17 hits per line

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

71.43
/src/LanguageServerManager.ts
1
import type { LanguageClientOptions, ServerOptions, ExecuteCommandParams, StateChangeEvent } from 'vscode-languageclient/node';
2
import { LanguageClient, State, TransportKind } from 'vscode-languageclient/node';
1✔
3
import * as vscode from 'vscode';
1✔
4
import * as path from 'path';
1✔
5
import type { Disposable } from 'vscode';
6
import { window, workspace } from 'vscode';
1✔
7
import { BusyStatus, NotificationName, standardizePath as s } from 'brighterscript';
1✔
8
import { CustomCommands, Deferred } from 'brighterscript';
1✔
9
import { logger as extensionLogger } from './extensionLogger';
1✔
10

11
const logger = extensionLogger.createLogger('LanguageServerManager');
1✔
12
import type { CodeWithSourceMap } from 'source-map';
13
import BrightScriptDefinitionProvider from './BrightScriptDefinitionProvider';
1✔
14
import { BrightScriptWorkspaceSymbolProvider, SymbolInformationRepository } from './SymbolInformationRepository';
1✔
15
import { BrightScriptDocumentSymbolProvider } from './BrightScriptDocumentSymbolProvider';
1✔
16
import { BrightScriptReferenceProvider } from './BrightScriptReferenceProvider';
1✔
17
import BrightScriptSignatureHelpProvider from './BrightScriptSignatureHelpProvider';
1✔
18
import type { DefinitionRepository } from './DefinitionRepository';
19
import { util } from './util';
1✔
20
import { LanguageServerInfoCommand, languageServerInfoCommand } from './commands/LanguageServerInfoCommand';
1✔
21
import * as fsExtra from 'fs-extra';
1✔
22
import { EventEmitter } from 'eventemitter3';
1✔
23
import * as dayjs from 'dayjs';
1✔
24
import type { LocalPackageManager, ParsedVersionInfo } from './managers/LocalPackageManager';
25
import { firstBy } from 'thenby';
1✔
26

27
/**
28
 * Tracks the running/stopped state of the language server. When the lsp crashes, vscode will restart it. After the 5th crash, they'll leave it permanently crashed.
29
 * There seems to be no time limit on adding up to the 5, so even after a few days, vscode may still terminate the language server.
30
 * This class track when the language server is stopped and then not started back up again after a period of time.
31
 * For example, 20 seconds after after the final failure, this event fires so that we can show a "wanna restart it" popup.
32
 */
33
class LspRunTracker {
34

35
    public constructor(
36
        public debounceDelay: number
33✔
37
    ) {
38
    }
39

40
    public setState(state: State) {
41
        //if language server is running, clear any timers
42
        if (state === State.Starting || state === State.Running) {
1!
43
            clearTimeout(this.timeoutHandle);
×
44
        } else {
45
            this.timeoutHandle = setTimeout(() => {
1✔
46
                clearTimeout(this.timeoutHandle);
1✔
47
                this.emitter.emit('stopped');
1✔
48
            }, this.debounceDelay);
49
        }
50
    }
51
    private timeoutHandle: NodeJS.Timeout;
52

53
    private emitter = new EventEmitter();
33✔
54
    public on(event: 'stopped', listener: () => any) {
55
        this.emitter.on(event, listener);
2✔
56
        return () => {
2✔
57
            this.emitter.off(event, listener);
×
58
        };
59
    }
60
}
61

62
export const LANGUAGE_SERVER_NAME = 'BrighterScript Language Server';
1✔
63

64
export class LanguageServerManager {
1✔
65
    constructor() {
66
        this.deferred = new Deferred();
33✔
67
        const brighterscriptDir = require.resolve('brighterscript').replace(/[\\\/]dist[\\\/]index.js/i, '');
33✔
68
        const version = fsExtra.readJsonSync(`${brighterscriptDir}/package.json`).version;
33✔
69
        this.embeddedBscInfo = {
33✔
70
            packageDir: brighterscriptDir,
71
            versionInfo: version,
72
            version: version
73
        };
74
        //default to the embedded bsc version
75
        this.selectedBscInfo = this.embeddedBscInfo;
33✔
76
    }
77

78
    /**
79
     * Information about the embedded brighterscript version
80
     */
81
    public embeddedBscInfo: BscInfo;
82
    /**
83
     * Information about the currently selected brighterscript version (the one that's running right now)
84
     */
85
    public selectedBscInfo: BscInfo;
86

87
    private context: vscode.ExtensionContext;
88
    private definitionRepository: DefinitionRepository;
89
    private get declarationProvider() {
90
        return this.definitionRepository.provider;
8✔
91
    }
92

93
    private localPackageManager: LocalPackageManager;
94

95
    /**
96
     * The delay after init before we delete any outdated bsc versions
97
     */
98
    private outdatedBscVersionDeleteDelay = 5 * 60 * 1000;
33✔
99

100
    public async init(
101
        context: vscode.ExtensionContext,
102
        definitionRepository: DefinitionRepository,
103
        localPackageManager: LocalPackageManager
104
    ) {
105
        this.context = context;
2✔
106

107
        this.definitionRepository = definitionRepository;
2✔
108

109
        this.localPackageManager = localPackageManager;
2✔
110

111
        //anytime the window changes focus, save the current brighterscript version
112
        vscode.window.onDidChangeWindowState(async (e) => {
2✔
113
            await this.localPackageManager.setUsage('brighterscript', this.selectedBscInfo.versionInfo);
×
114
        });
115

116
        //in about 5 minutes, clean up any outdated bsc versions (delayed to prevent slower startup times)
117
        setTimeout(() => {
2✔
118
            void this.deleteOutdatedBscVersions();
2✔
119
        }, this.outdatedBscVersionDeleteDelay);
120

121
        //if the lsp is permanently stopped by vscode, ask the user if they want to restart it again.
122
        this.lspRunTracker.on('stopped', async () => {
2✔
123
            //stop the statusbar spinner
124
            this.updateStatusbar(false);
1✔
125
            if (this.isLanguageServerEnabledInSettings()) {
1!
126
                const response = await vscode.window.showErrorMessage('The BrighterScript language server unexpectedly shut down. Do you want to restart it?', {
1✔
127
                    modal: true
128
                }, { title: 'Yes' }, { title: 'No ', isCloseAffordance: true });
129
                if (response.title === 'Yes') {
1!
130
                    await this.restart();
×
131
                }
132
            } else {
133
                await this.disableLanguageServer();
×
134
            }
135
        });
136

137
        //dynamically enable or disable the language server based on user settings
138
        vscode.workspace.onDidChangeConfiguration(async (configuration) => {
2✔
139
            //if we've changed the bsdk setting, restart the language server
140
            if (configuration.affectsConfiguration('brightscript.bsdk')) {
×
141
                await this.syncVersionAndTryRun();
×
142
            }
143

144
            //if the language server enable setting changed, restart the language server
145
            if (configuration.affectsConfiguration('brightscript.enableLanguageServer') ||
×
146
                configuration.affectsConfiguration('brightscript.languageServer.enabled')) {
147
                await this.syncVersionAndTryRun();
×
148
            }
149
        });
150
        await this.syncVersionAndTryRun();
2✔
151
    }
152

153
    private deferred: Deferred<any>;
154

155
    /**
156
     * Returns a promise that resolves once the language server is ready to be interacted with
157
     */
158
    private async ready() {
159
        if (this.isLanguageServerEnabledInSettings() === false) {
1!
160
            throw new Error('Language server is disabled in user settings');
×
161
        } else {
162
            return this.deferred.promise;
1✔
163
        }
164
    }
165

166
    private refreshDeferred() {
167
        let newDeferred = new Deferred<any>();
3✔
168
        //chain any pending promises to this new deferred
169
        if (!this.deferred.isCompleted) {
3✔
170
            this.deferred.resolve(newDeferred.promise);
2✔
171
        }
172
        this.deferred = newDeferred;
3✔
173
    }
174

175
    private client: LanguageClient;
176
    private statusbarItem: vscode.StatusBarItem;
177

178
    private lspRunTracker = new LspRunTracker(20_000);
33✔
179

180
    private clientDispose: Disposable;
181

182
    /**
183
     * Create a new LanguageClient instance
184
     * @returns
185
     */
186
    private constructLanguageClient() {
187

188
        // The server is implemented in node
189
        let serverModule = this.context.asAbsolutePath(
×
190
            path.join('dist', 'LanguageServerRunner.js')
191
        );
192

193
        //give the runner the specific version of bsc to run
194
        const args = [
×
195
            this.selectedBscInfo.packageDir,
196
            (this.context.extensionMode === vscode.ExtensionMode.Development).toString()
197
        ];
198
        // If the extension is launched in debug mode then the debug server options are used
199
        // Otherwise the run options are used
200
        let serverOptions: ServerOptions = {
×
201
            run: {
202
                module: serverModule,
203
                transport: TransportKind.ipc,
204
                args: args
205
            },
206
            debug: {
207
                module: serverModule,
208
                transport: TransportKind.ipc,
209
                args: args,
210
                // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
211
                options: { execArgv: ['--nolazy', '--inspect=6009'] }
212
            }
213
        };
214

215
        // Options to control the language client
216
        let clientOptions: LanguageClientOptions = {
×
217
            // Register the server for various types of documents
218
            documentSelector: [
219
                { scheme: 'file', language: 'brightscript' },
220
                { scheme: 'file', language: 'brighterscript' },
221
                { scheme: 'file', language: 'xml' }
222
            ],
223
            synchronize: {
224
                // Notify the server about file changes to every filetype it cares about
225
                fileEvents: workspace.createFileSystemWatcher('**/*')
226
            },
227
            middleware: {
228
                workspace: {
229
                    //apply willRenameFiles edits silently (matches the TypeScript extension's behavior).
230
                    //Default vscode-languageclient routes the edit through VS Code's onWillRenameFiles -> bulkEditService,
231
                    //which shows a "wants to make refactoring changes" modal. Apply manually with no metadata to skip it,
232
                    //then save the affected documents so they don't end up dirty.
233
                    willRenameFiles: async (event, next) => {
234
                        const edit = await next(event);
×
235
                        if (!edit || (!edit.entries || edit.entries().length === 0)) {
×
236
                            return undefined;
×
237
                        }
238
                        await vscode.workspace.applyEdit(edit);
×
239
                        const affectedUris = edit.entries().map(([uri]) => uri.toString());
×
240
                        await Promise.all(
×
241
                            vscode.workspace.textDocuments
242
                                .filter(doc => doc.isDirty && affectedUris.includes(doc.uri.toString()))
×
243
                                .map(doc => doc.save())
×
244
                        );
245
                        //return undefined so vscode-languageclient doesn't ask VS Code to apply the edit a second time via the modal
246
                        return undefined;
×
247
                    }
248
                }
249
            }
250
        };
251

252
        // Create the language client and start the client.
253
        return new LanguageClient(
×
254
            'brighterScriptLanguageServer',
255
            LANGUAGE_SERVER_NAME,
256
            serverOptions,
257
            clientOptions
258
        );
259
    }
260

261
    private async enableLanguageServer() {
262
        try {
3✔
263
            //if we already have a language server, nothing more needs to be done
264
            if (this.client) {
3✔
265
                return await this.ready();
1✔
266
            }
267
            this.refreshDeferred();
2✔
268

269
            //create the statusbar item
270
            this.statusbarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
2✔
271
            this.statusbarItem.command = LanguageServerInfoCommand.commandName;
2✔
272

273
            //enable the statusbar loading anmation. the language server will disable once it finishes loading
274
            this.updateStatusbar(false);
2✔
275

276
            this.statusbarItem.show();
2✔
277

278
            //disable the simple providers (the language server will handle all of these)
279
            this.disableSimpleProviders();
2✔
280

281
            this.client = this.constructLanguageClient();
2✔
282

283
            this.client.onDidChangeState((event: StateChangeEvent) => {
2✔
284
                logger.info('onDidChangeState', State[event.newState]);
1✔
285
                this.lspRunTracker.setState(event.newState);
1✔
286
            });
287

288
            // Start the client. This will also launch the server
289
            this.clientDispose = this.client.start();
2✔
290

291
            await this.client.onReady();
2✔
292

293
            this.client.onNotification('critical-failure', (message) => {
2✔
294
                void window.showErrorMessage(message);
×
295
            });
296
            this.registerBusyStatusHandler();
2✔
297
            this.deferred.resolve(true);
2✔
298
        } catch (e) {
299
            //stop the client by any means necessary
300
            try {
1✔
301
                void this.client?.stop?.();
1!
302
            } catch { }
303
            delete this.client;
1✔
304

305
            this.refreshDeferred();
1✔
306

307
            this.deferred?.reject(e);
1!
308
            throw e;
1✔
309
        }
310
        return this.ready();
2✔
311
    }
312

313
    /**
314
     * How many milliseconds to wait before showing a warning about the LSP being busy for too long
315
     */
316
    private busyStatusWarningThreshold = 60_000;
33✔
317

318
    private registerBusyStatusHandler() {
319
        let timeoutHandle: NodeJS.Timeout;
320

321
        this.client.onNotification(NotificationName.busyStatus, (event: any) => {
3✔
322
            this.updateStatusbar(event.status === BusyStatus.busy, event.activeRuns);
4✔
323

324
            //clear any existing timeout
325
            if (timeoutHandle) {
4✔
326
                clearTimeout(timeoutHandle);
3✔
327
            }
328

329
            //if the busy status takes too long, write a lsp log entry with details of what's still pending
330
            if (event.status === BusyStatus.busy) {
4!
331
                timeoutHandle = setTimeout(() => {
4✔
332
                    const delay = Date.now() - event.timestamp;
1✔
333
                    this.client.outputChannel.appendLine(`${logger.formatTimestamp(new Date())} language server has been 'busy' for ${delay}ms. most recent busyStatus event: ${JSON.stringify(event, undefined, 4)}`);
1✔
334
                }, this.busyStatusWarningThreshold);
335
            }
336
        });
337
    }
338

339
    /**
340
     * Enable/disable the loading spinner on the statusbar item
341
     */
342
    private updateStatusbar(isLoading: boolean, activeRuns?: ActiveRun[]) {
343
        //do nothing if we don't have a statusbar
344
        if (!this.statusbarItem) {
9✔
345
            return;
6✔
346
        }
347
        const icon = isLoading ? '$(sync~spin)' : '$(flame)';
3!
348
        this.statusbarItem.text = `${icon} bsc-${this.selectedBscInfo.version}`;
3✔
349
        let tooltip = `BrightScript Language Server: ${isLoading ? 'working' : 'idle'}`;
3!
350

351
        //print any acdtive runs so devs know what is taking so long
352
        if (activeRuns?.length > 0) {
3!
353
            tooltip += '\n' + this.getActiveRunsTooltipText(activeRuns);
×
354
        }
355
        this.statusbarItem.tooltip = tooltip;
3✔
356
    }
357

358
    private getActiveRunsTooltipText(activeRuns: ActiveRun[]) {
359
        let tooltip = '';
4✔
360

361
        //sort the runs so they're more consistent
362
        const groups = activeRuns?.sort?.(
4✔
363
            firstBy('scope').thenBy('label').thenBy('startTime')
364
        ).reduce((groups, item) => {
365
            if (!groups.has(item.scope)) {
2!
366
                groups.set(item.scope, []);
2✔
367
            }
368
            groups.get(item.scope).push(item);
2✔
369
            return groups;
2✔
370
        }, new Map<string, Array<typeof activeRuns[0]>>());
371

372
        for (let [groupName, runsForGroup] of groups ?? []) {
4✔
373
            if (!groupName || groupName?.trim() === 'undefined') {
2!
374
                groupName = 'general';
1✔
375
            }
376

377
            if (groupName) {
2!
378
                tooltip += `\n${groupName}:`;
2✔
379
            }
380

381
            for (const run of runsForGroup ?? []) {
2!
382
                let line = '\n\t';
2✔
383

384
                line += `${run.label} (since ${new Date(run.startTime)?.toLocaleTimeString()})`;
2!
385
                tooltip += line;
2✔
386
            }
387
        }
388
        return tooltip;
4✔
389
    }
390

391
    /**
392
     * Stop and then start the language server.
393
     * This is a noop if the language server is currently disabled
394
     */
395
    public async restart() {
396
        await this.disableLanguageServer();
×
397
        await util.delay(1);
×
398
        await this.syncVersionAndTryRun();
×
399
    }
400

401
    private async disableLanguageServer() {
402
        if (this.client) {
×
403
            await this.client.stop();
×
404
            this.statusbarItem.dispose();
×
405
            this.statusbarItem = undefined;
×
406
            this.clientDispose?.dispose();
×
407
            this.client = undefined;
×
408
            //delay slightly to let things catch up
409
            await util.delay(100);
×
410
            this.deferred = new Deferred();
×
411
        }
412
        //enable the simple providers (since there is no language server)
413
        this.enableSimpleProviders();
×
414
    }
415

416
    private simpleSubscriptions = [] as Disposable[];
33✔
417

418
    /**
419
     * Enable the simple providers (which means the language server is disabled).
420
     * These were the original providers created by George. Most of this functionality has been moved into the language server
421
     * However, if the language server is disabled, we want to at least fall back to these.
422
     */
423
    private enableSimpleProviders() {
424
        if (this.simpleSubscriptions.length === 0) {
4!
425
            //register the definition provider
426
            const definitionProvider = new BrightScriptDefinitionProvider(this.definitionRepository);
4✔
427
            const symbolInformationRepository = new SymbolInformationRepository(this.declarationProvider);
4✔
428
            const selector = { scheme: 'file', pattern: '**/*.{brs,bs}' };
4✔
429

430
            this.simpleSubscriptions.push(
4✔
431
                vscode.languages.registerDefinitionProvider(selector, definitionProvider),
432
                vscode.languages.registerDocumentSymbolProvider(selector, new BrightScriptDocumentSymbolProvider(this.declarationProvider)),
433
                vscode.languages.registerWorkspaceSymbolProvider(new BrightScriptWorkspaceSymbolProvider(symbolInformationRepository)),
434
                vscode.languages.registerReferenceProvider(selector, new BrightScriptReferenceProvider()),
435
                vscode.languages.registerSignatureHelpProvider(selector, new BrightScriptSignatureHelpProvider(this.definitionRepository), '(', ',')
436
            );
437

438
            this.context.subscriptions.push(...this.simpleSubscriptions);
4✔
439
        }
440
    }
441

442
    /**
443
     * Disable the simple subscriptions (which means we'll depend on the language server)
444
     */
445
    private disableSimpleProviders() {
446
        if (this.simpleSubscriptions.length > 0) {
2!
447
            for (const sub of this.simpleSubscriptions) {
×
448
                const idx = this.context.subscriptions.indexOf(sub);
×
449
                if (idx > -1) {
×
450
                    this.context.subscriptions.splice(idx, 1);
×
451
                    sub.dispose();
×
452
                }
453
            }
454
            this.simpleSubscriptions = [];
×
455
        }
456
    }
457

458
    public isLanguageServerEnabledInSettings() {
459
        const result = util.getConfigurationValueIfDefined('brightscript.languageServer.enabled') ?? util.getConfigurationValueIfDefined('brightscript.enableLanguageServer', true);
10✔
460
        return result;
10✔
461
    }
462

463
    public async getTranspiledFileContents(pathAbsolute: string) {
464
        //wait for the language server to be ready
465
        await this.ready();
×
466
        let result = await this.client.sendRequest('workspace/executeCommand', {
×
467
            command: CustomCommands.TranspileFile,
468
            arguments: [pathAbsolute]
469
        } as ExecuteCommandParams);
470
        return result as CodeWithSourceMap;
×
471
    }
472

473
    /**
474
     * Check user settings for which language server version to use,
475
     * and if different, re-launch the specific version of the language server'
476
     */
477
    public async syncVersionAndTryRun() {
478
        const versionInfo = await this.getBsdkVersionInfo();
×
479

480
        //if the path to bsc is different, spin down the old server and start a new one
481
        if (versionInfo !== this.selectedBscInfo.packageDir) {
×
482
            await this.disableLanguageServer();
×
483
        }
484

485
        //ensure the version of the language server is installed and available
486

487
        //try to load the package version.
488
        try {
×
489
            this.selectedBscInfo = await this.ensureBscVersionInstalled(versionInfo);
×
490
        } catch (e) {
NEW
491
            logger.error(e);
×
492
            //fall back to the embedded version, and show a popup (don't await the popup because that blocks this flow)
493
            void vscode.window.showErrorMessage(`Language server failure. Did you forget \`npm install\`? Using embedded version ${this.embeddedBscInfo.version}. Can't find language server for "${versionInfo}"`);
×
494
            this.selectedBscInfo = this.embeddedBscInfo;
×
495
        }
496

497
        if (this.isLanguageServerEnabledInSettings()) {
×
498
            await this.enableLanguageServer();
×
499
        } else {
500
            await this.disableLanguageServer();
×
501
        }
502
    }
503

504
    public parseVersionInfo(versionInfo: string, cwd = process.cwd()): ParsedVersionInfo {
6✔
505
        if (versionInfo === 'embedded') {
14✔
506
            return {
2✔
507
                type: 'dir',
508
                value: s`${this.embeddedBscInfo.packageDir}`
509
            };
510
        } else {
511
            return this.localPackageManager.parseVersionInfo(versionInfo, cwd);
12✔
512
        }
513
    }
514

515
    /**
516
     * Get the value for `brightscript.bsdk` from the following locations (in order). First one found wins:
517
     * - use `brightscript.bsdk` value from the current `.code-workspace` file
518
     * - if there is only 1 workspaceFolder with a `brightscript.bsdk` value, use that.
519
     * - if there are multiple workspace folders with `brightscript.bsdk` values, prompt the user to pick which one to use
520
     * - if there are no `brightscript.bsdk` values, use the embedded version
521
     * @returns an absolute path to a directory for the bsdk, or the non-path value (i.e. a URL or a version number)
522
     */
523
    private async getBsdkVersionInfo(): Promise<string> {
524

525
        //use bsdk entry in the code-workspace file
526
        if (this.workspaceConfigIncludesBsdkKey()) {
9✔
527
            let result = this.parseVersionInfo(
2✔
528
                util.getConfiguration('brightscript', vscode.workspace.workspaceFile).get<string>('bsdk')?.trim?.(),
12!
529
                path.dirname(vscode.workspace.workspaceFile.fsPath)
530
            );
531
            if (result) {
2!
532
                return result.value;
2✔
533
            }
534
        }
535

536
        //collect `brightscript.bsdk` setting value from each workspaceFolder
537
        const folderResults = vscode.workspace.workspaceFolders?.reduce((acc, workspaceFolder) => {
7!
538
            const versionInfo = util.getConfiguration('brightscript', workspaceFolder).get<string>('bsdk');
6✔
539
            const parsed = this.parseVersionInfo(versionInfo, workspaceFolder.uri.fsPath);
6✔
540
            if (parsed) {
6✔
541
                acc.set(parsed.value, parsed);
5✔
542
            }
543
            return acc;
6✔
544
        }, new Map<string, ParsedVersionInfo>()) ?? new Map<string, ParsedVersionInfo>();
545

546
        //no results found, use the embedded version
547
        if (folderResults.size === 0) {
7✔
548
            return this.embeddedBscInfo.packageDir;
3✔
549

550
            //we have exactly one result. use it
551
        } else if (folderResults.size === 1) {
4✔
552
            return [...folderResults.values()][0].value;
3✔
553

554
            //there were multiple versions. make the user pick which to use
555
        } else {
556
            //TODO should we prompt for just these items?
557
            return languageServerInfoCommand.selectBrighterScriptVersion();
1✔
558
        }
559
    }
560

561
    private workspaceConfigIncludesBsdkKey() {
562
        return vscode.workspace.workspaceFile &&
9✔
563
            fsExtra.pathExistsSync(vscode.workspace.workspaceFile.fsPath) &&
564
            /"brightscript.bsdk"/.exec(
565
                fsExtra.readFileSync(vscode.workspace.workspaceFile.fsPath
566
                ).toString()
567
            );
568
    }
569

570
    /**
571
     * Ensure that the specified bsc version is installed in the global storage directory.
572
     * @param version
573
     * @param retryCount the number of times we should retry before giving up
574
     * @returns full path to the root of where the brighterscript module is installed
575
     */
576
    @OneAtATime({ timeout: 3 * 60 * 1000 })
577
    private async ensureBscVersionInstalled(versionInfo: string, retryCount = 1, showProgress = true): Promise<BscInfo> {
1!
578
        const parsed = this.parseVersionInfo(versionInfo);
6✔
579

580
        //if this is a directory, use it as-is
581
        if (parsed.type === 'dir') {
6✔
582
            //if the directory does not exist, throw an error
583
            if (await fsExtra.pathExists(s`${parsed.value}/package.json`) === false) {
2✔
584
                throw new Error(`"${parsed.value}" does not contain a package.json file`);
1✔
585
            }
586
            return {
1✔
587
                packageDir: s`${parsed.value}`,
588
                version: fsExtra.readJsonSync(s`${parsed.value}/package.json`, { throws: false })?.version ?? parsed.value,
6!
589
                versionInfo: versionInfo
590
            };
591
        }
592

593
        //install this version of brighterscript
594
        try {
4✔
595
            const packageInfo = await util.runWithProgress({
4✔
596
                title: 'Installing brighterscript language server ' + versionInfo,
597
                location: vscode.ProgressLocation.Notification,
598
                cancellable: false,
599
                //show a progress spinner if configured to do so
600
                showProgress: showProgress && !this.localPackageManager.isInstalled('brighterscript', versionInfo)
8✔
601
            }, async () => {
602
                return this.localPackageManager.install('brighterscript', versionInfo);
4✔
603
            });
604
            return {
3✔
605
                packageDir: packageInfo.packageDir,
606
                version: packageInfo.version,
607
                versionInfo: versionInfo
608
            };
609

610
        } catch (e) {
611
            if (retryCount > 0) {
1!
612
                logger.error('Failed to install brighterscript', versionInfo, e);
1✔
613

614
                //if the install failed for some reason, uninstall the package and try again
615
                await this.localPackageManager.uninstall('brighterscript', versionInfo);
1✔
616
                return await this.ensureBscVersionInstalled(versionInfo, retryCount - 1, showProgress);
1✔
617
            } else {
618
                throw e;
×
619
            }
620
        }
621
    }
622

623
    /**
624
     * Delete any brighterscript versions that haven't been used in a while
625
     */
626
    private async deleteOutdatedBscVersions() {
627
        const npmCacheRetentionDays = util.getConfiguration('brightscript')?.get?.('npmCacheRetentionDays', 45) ?? 45;
1!
628

629
        //build the cutoff date (i.e. 45 days ago)
630
        const cutoffDate = dayjs().subtract(npmCacheRetentionDays, 'days');
1✔
631
        await this.localPackageManager.deletePackagesNotUsedSince(cutoffDate.toDate());
1✔
632
    }
633
}
634

635
export const languageServerManager = new LanguageServerManager();
1✔
636

637
interface BscInfo {
638
    /**
639
     * The full path to the brighterscript module (i.e. the folder where its `package.json` is located
640
     */
641
    packageDir: string;
642
    /**
643
     * The versionInfo of the brighterscript module. Typically this is a semantic version, but it could be a URL or a folder path.
644
     * Anything that can go inside a `package.json` file is acceptable as well
645
     */
646
    versionInfo: string;
647
    /**
648
     * The version of the brighterscript module from its package.json. This is displayed in the statusbar
649
     */
650
    version: string;
651
}
652

653

654
/**
655
 * Force method calls to run one-at-a-time, waiting for the completion of the previous call before running the next.
656
 */
657
function OneAtATime(options: { timeout?: number }) {
658
    return function OneAtATime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
1✔
659
        let originalMethod = descriptor.value;
1✔
660

661
        //wrap the original method
662
        descriptor.value = function value(...args: any[]) {
1✔
663
            //ensure the promise structure exists for this call
664
            target.__oneAtATime ??= {};
6✔
665
            target.__oneAtATime[propertyKey] ??= Promise.resolve();
6✔
666

667
            const timer = util.sleep(options.timeout > 0 ? options.timeout : Number.MAX_SAFE_INTEGER);
6!
668

669
            return Promise.race([
6✔
670
                //race for the last task to resolve
671
                target.__oneAtATime[propertyKey].finally(() => {
672
                    timer?.cancel?.();
6!
673
                }),
674
                //race for the timeout to expire (we give up waiting for the previous task to complete)
675
                timer.then(() => {
676
                    //our timer fired before we had a chance to cancel it. Report the error and move on
NEW
677
                    logger.error(`timer expired waiting for the previous ${propertyKey} to complete. Running the next instance`, target);
×
678
                })
679
                //now we can move on to the actual task
680
            ]).then(() => {
681
                return originalMethod.apply(this, args);
6✔
682
            });
683
        };
684
    };
685
}
686
interface ActiveRun {
687
    label: string;
688
    startTime: number;
689
    scope?: string;
690
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc