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

rokucommunity / brighterscript / #13903

11 Oct 2024 04:57PM UTC coverage: 88.99% (+0.8%) from 88.149%
#13903

push

TwitchBronBron
Better settings reload functionality

7194 of 8520 branches covered (84.44%)

Branch coverage included in aggregate %.

34 of 35 new or added lines in 2 files covered. (97.14%)

274 existing lines in 18 files now uncovered.

9601 of 10353 relevant lines covered (92.74%)

1780.31 hits per line

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

84.96
/src/LanguageServer.ts
1
import * as path from 'path';
1✔
2
import 'array-flat-polyfill';
1✔
3
import type {
4
    CompletionItem,
5
    Connection,
6
    DidChangeWatchedFilesParams,
7
    InitializeParams,
8
    ServerCapabilities,
9
    TextDocumentPositionParams,
10
    ExecuteCommandParams,
11
    WorkspaceSymbolParams,
12
    DocumentSymbolParams,
13
    ReferenceParams,
14
    SignatureHelpParams,
15
    CodeActionParams,
16
    SemanticTokens,
17
    SemanticTokensParams,
18
    TextDocumentChangeEvent,
19
    HandlerResult,
20
    InitializeError,
21
    InitializeResult,
22
    CompletionParams,
23
    ResultProgressReporter,
24
    WorkDoneProgressReporter,
25
    SemanticTokensOptions,
26
    CompletionList,
27
    CancellationToken,
28
    DidChangeConfigurationParams,
29
    DidChangeConfigurationRegistrationOptions
30
} from 'vscode-languageserver/node';
31
import {
1✔
32
    SemanticTokensRequest,
33
    createConnection,
34
    DidChangeConfigurationNotification,
35
    FileChangeType,
36
    ProposedFeatures,
37
    TextDocuments,
38
    TextDocumentSyncKind,
39
    CodeActionKind
40
} from 'vscode-languageserver/node';
41
import { URI } from 'vscode-uri';
1✔
42
import { TextDocument } from 'vscode-languageserver-textdocument';
1✔
43
import { util } from './util';
1✔
44
import { DiagnosticCollection } from './DiagnosticCollection';
1✔
45
import { encodeSemanticTokens, semanticTokensLegend } from './SemanticTokenUtils';
1✔
46
import { LogLevel, createLogger, logger, setLspLoggerProps } from './logging';
1✔
47
import ignore from 'ignore';
1✔
48
import * as micromatch from 'micromatch';
1✔
49
import type { LspProject, LspDiagnostic } from './lsp/LspProject';
50
import { PathFilterer } from './lsp/PathFilterer';
1✔
51
import type { WorkspaceConfig } from './lsp/ProjectManager';
52
import { ProjectManager } from './lsp/ProjectManager';
1✔
53
import * as fsExtra from 'fs-extra';
1✔
54
import type { MaybePromise } from './interfaces';
55
import { workerPool } from './lsp/worker/WorkerThreadProject';
1✔
56
// eslint-disable-next-line @typescript-eslint/no-require-imports
57
import isEqual = require('lodash.isequal');
1✔
58

59
export class LanguageServer {
1✔
60
    /**
61
     * The default threading setting for the language server. Can be overridden by per-workspace settings
62
     */
63
    public static enableThreadingDefault = true;
1✔
64
    /**
65
     * The language server protocol connection, used to send and receive all requests and responses
66
     */
67
    private connection = undefined as Connection;
47✔
68

69
    /**
70
     * Manages all projects for this language server
71
     */
72
    private projectManager: ProjectManager;
73

74
    private hasConfigurationCapability = false;
47✔
75

76
    /**
77
     * Indicates whether the client supports workspace folders
78
     */
79
    private clientHasWorkspaceFolderCapability = false;
47✔
80

81
    /**
82
     * Create a simple text document manager.
83
     * The text document manager supports full document sync only
84
     */
85
    private documents = new TextDocuments(TextDocument);
47✔
86

87
    private loggerSubscription: () => void;
88

89
    /**
90
     * Used to filter paths based on include/exclude lists (like .gitignore or vscode's `files.exclude`).
91
     * This is used to prevent the language server from being overwhelmed by files we don't actually want to handle
92
     */
93
    private pathFilterer: PathFilterer;
94

95
    public logger = createLogger({
47✔
96
        logLevel: LogLevel.log
97
    });
98

99
    constructor() {
100
        setLspLoggerProps();
47✔
101
        //replace the workerPool logger with our own so logging info can be synced
102
        workerPool.logger = this.logger.createLogger();
47✔
103

104
        this.pathFilterer = new PathFilterer({ logger: this.logger });
47✔
105

106
        this.projectManager = new ProjectManager({
47✔
107
            pathFilterer: this.pathFilterer,
108
            logger: this.logger.createLogger()
109
        });
110

111
        //anytime a project emits a collection of diagnostics, send them to the client
112
        this.projectManager.on('diagnostics', (event) => {
47✔
113
            this.logger.debug(`Received ${event.diagnostics.length} diagnostics from project ${event.project.projectNumber}`);
48✔
114
            this.sendDiagnostics(event).catch(logAndIgnoreError);
48✔
115
        });
116

117
        // Send all open document changes whenever a project is activated. This is necessary because at project startup, the project loads files from disk
118
        // and may not have the latest unsaved file changes. Any existing projects that already use these files will just ignore the changes
119
        // because the file contents haven't changed.
120

121
        this.projectManager.on('project-activate', (event) => {
47✔
122
            //keep logLevel in sync with the most verbose log level found across all projects
123
            this.syncLogLevel().catch(logAndIgnoreError);
38✔
124

125
            //resend all open document changes
126
            const documents = [...this.documents.all()];
38✔
127
            if (documents.length > 0) {
38✔
128
                this.logger.log(`Project ${event.project?.projectNumber} loaded or changed. Resending all open document changes.`, documents.map(x => x.uri));
41✔
129
                for (const document of this.documents.all()) {
20✔
130
                    this.onTextDocumentDidChangeContent({
41✔
131
                        document: document
132
                    }).catch(logAndIgnoreError);
133
                }
134
            }
135
        });
136

137
        this.projectManager.busyStatusTracker.on('change', (event) => {
47✔
138
            this.sendBusyStatus();
222✔
139
        });
140
    }
141

142
    //run the server
143
    public run() {
144
        // Create a connection for the server. The connection uses Node's IPC as a transport.
145
        this.connection = this.establishConnection();
17✔
146

147
        //disable logger colors when running in LSP mode
148
        logger.enableColor = false;
17✔
149

150
        //listen to all of the output log events and pipe them into the debug channel in the extension
151
        this.loggerSubscription = logger.subscribe((message) => {
17✔
152
            this.connection.tracer.log(message.argsText);
110✔
153
        });
154

155
        //bind all our on* methods that share the same name from connection
156
        for (const name of Object.getOwnPropertyNames(LanguageServer.prototype)) {
17✔
157
            if (/on+/.test(name) && typeof this.connection?.[name] === 'function') {
493!
158
                this.connection[name](this[name].bind(this));
238✔
159
            }
160
        }
161

162
        //Register semantic token requests. TODO switch to a more specific connection function call once they actually add it
163
        this.connection.onRequest(SemanticTokensRequest.method, this.onFullSemanticTokens.bind(this));
17✔
164

165
        // The content of a text document has changed. This event is emitted
166
        // when the text document is first opened, when its content has changed,
167
        // or when document is closed without saving (original contents are sent as a change)
168
        //
169
        this.documents.onDidChangeContent(this.onTextDocumentDidChangeContent.bind(this));
17✔
170

171
        //whenever a document gets closed
172
        this.documents.onDidClose(this.onDocumentClose.bind(this));
17✔
173

174
        // listen for open, change and close text document events
175
        this.documents.listen(this.connection);
17✔
176

177
        // Listen on the connection
178
        this.connection.listen();
17✔
179
    }
180

181
    /**
182
     * Called when the client starts initialization
183
     */
184
    @AddStackToErrorMessage
185
    public onInitialize(params: InitializeParams): HandlerResult<InitializeResult, InitializeError> {
1✔
186
        let clientCapabilities = params.capabilities;
1✔
187

188
        // Does the client support the `workspace/configuration` request?
189
        // If not, we will fall back using global settings
190
        this.hasConfigurationCapability = !!(clientCapabilities.workspace && !!clientCapabilities.workspace.configuration);
1✔
191
        this.clientHasWorkspaceFolderCapability = !!(clientCapabilities.workspace && !!clientCapabilities.workspace.workspaceFolders);
1✔
192

193
        //return the capabilities of the server
194
        return {
1✔
195
            capabilities: {
196
                textDocumentSync: TextDocumentSyncKind.Full,
197
                // Tell the client that the server supports code completion
198
                completionProvider: {
199
                    resolveProvider: false,
200
                    //anytime the user types a period, auto-show the completion results
201
                    triggerCharacters: ['.'],
202
                    allCommitCharacters: ['.', '@']
203
                },
204
                documentSymbolProvider: true,
205
                workspaceSymbolProvider: true,
206
                semanticTokensProvider: {
207
                    legend: semanticTokensLegend,
208
                    full: true
209
                } as SemanticTokensOptions,
210
                referencesProvider: true,
211
                codeActionProvider: {
212
                    codeActionKinds: [CodeActionKind.Refactor]
213
                },
214
                signatureHelpProvider: {
215
                    triggerCharacters: ['(', ',']
216
                },
217
                definitionProvider: true,
218
                hoverProvider: true,
219
                executeCommandProvider: {
220
                    commands: [
221
                        CustomCommands.TranspileFile
222
                    ]
223
                }
224
            } as ServerCapabilities
225
        };
226
    }
227

228
    /**
229
     * Called when the client has finished initializing
230
     */
231
    @AddStackToErrorMessage
232
    public async onInitialized() {
1✔
233
        this.logger.log('onInitialized');
3✔
234

235
        //cache a copy of all workspace configurations to use for comparison later
236
        this.workspaceConfigsCache = new Map(
3✔
237
            (await this.getWorkspaceConfigs()).map(x => [x.workspaceFolder, x])
3✔
238
        );
239

240
        //set our logger to the most verbose logLevel found across any project
241
        await this.syncLogLevel();
3✔
242

243
        try {
3✔
244
            if (this.hasConfigurationCapability) {
3✔
245
                // register for when the user changes workspace or user settings
246
                await this.connection.client.register(
2✔
247
                    DidChangeConfigurationNotification.type,
248
                    {
249
                        //we only care about when these settings sections change
250
                        section: [
251
                            'brightscript',
252
                            'files'
253
                        ]
254
                    } as DidChangeConfigurationRegistrationOptions
255
                );
256
            }
257

258
            //populate the path filterer with the client's include/exclude lists
259
            await this.rebuildPathFilterer();
3✔
260

261
            await this.syncProjects();
3✔
262

263
            if (this.clientHasWorkspaceFolderCapability) {
3✔
264
                //if the client changes their workspaces, we need to get our projects in sync
265
                this.connection.workspace.onDidChangeWorkspaceFolders(async (evt) => {
1✔
UNCOV
266
                    await this.syncProjects();
×
267
                });
268
            }
269
        } catch (e: any) {
UNCOV
270
            this.sendCriticalFailure(
×
271
                `Critical failure during BrighterScript language server startup.
272
                Please file a github issue and include the contents of the 'BrighterScript Language Server' output channel.
273

274
                Error message: ${e.message}`
275
            );
UNCOV
276
            throw e;
×
277
        }
278
    }
279

280
    /**
281
     * Set our logLevel to the most verbose log level found across all projects and workspaces
282
     */
283
    private async syncLogLevel() {
284
        /**
285
         * helper to get the logLevel from a list of items and return the item and level (if found), or undefined if not
286
         */
287
        const getLogLevel = async<T>(
78✔
288
            items: T[],
289
            fetcher: (item: T) => MaybePromise<LogLevel | string>
290
        ): Promise<{ logLevel: LogLevel; logLevelText: string; item: T }> => {
291
            const logLevels = await Promise.all(
149✔
292
                items.map(async (item) => {
293
                    let value = await fetcher(item);
163✔
294
                    //force string values to lower case (so we can support things like 'log' or 'Log' or 'LOG')
295
                    if (typeof value === 'string') {
163✔
296
                        value = value.toLowerCase();
10✔
297
                    }
298
                    const logLevelNumeric = this.logger.getLogLevelNumeric(value as any);
163✔
299

300
                    if (typeof logLevelNumeric === 'number') {
163✔
301
                        return logLevelNumeric;
86✔
302
                    } else {
303
                        return -1;
77✔
304
                    }
305
                })
306
            );
307
            let idx = logLevels.findIndex(x => x > -1);
149✔
308
            if (idx > -1) {
149✔
309
                const mostVerboseLogLevel = Math.max(...logLevels);
71✔
310
                return {
71✔
311
                    logLevel: mostVerboseLogLevel,
312
                    logLevelText: this.logger.getLogLevelText(mostVerboseLogLevel),
313
                    //find the first item having the most verbose logLevel
314
                    item: items[logLevels.findIndex(x => x === mostVerboseLogLevel)]
72✔
315
                };
316
            }
317
        };
318

319
        const workspaces = await this.getWorkspaceConfigs();
78✔
320

321
        let workspaceResult = await getLogLevel(workspaces, workspace => workspace?.languageServer?.logLevel);
81!
322

323
        if (workspaceResult) {
76✔
324
            this.logger.info(`Setting global logLevel to '${workspaceResult.logLevelText}' based on configuration from workspace '${workspaceResult?.item?.workspaceFolder}'`);
3!
325
            this.logger.logLevel = workspaceResult.logLevel;
3✔
326
            return;
3✔
327
        }
328

329
        let projectResult = await getLogLevel(this.projectManager.projects, (project) => project.logger.logLevel);
82✔
330
        if (projectResult) {
73✔
331
            this.logger.info(`Setting global logLevel to '${projectResult.logLevelText}' based on project #${projectResult?.item?.projectNumber}`);
68!
332
            this.logger.logLevel = projectResult.logLevel;
68✔
333
            return;
68✔
334
        }
335

336
        //use a default level if no other level was found
337
        this.logger.logLevel = LogLevel.log;
5✔
338
    }
339

340
    @AddStackToErrorMessage
341
    private async onTextDocumentDidChangeContent(event: TextDocumentChangeEvent<TextDocument>) {
1✔
342
        this.logger.debug('onTextDocumentDidChangeContent', event.document.uri);
42✔
343

344
        await this.projectManager.handleFileChanges([{
42✔
345
            srcPath: URI.parse(event.document.uri).fsPath,
346
            type: FileChangeType.Changed,
347
            fileContents: event.document.getText(),
348
            allowStandaloneProject: true
349
        }]);
350
    }
351

352
    /**
353
     * Called when watched files changed (add/change/delete).
354
     * The CLIENT is in charge of what files to watch, so all client
355
     * implementations should ensure that all valid project
356
     * file types are watched (.brs,.bs,.xml,manifest, and any json/text/image files)
357
     */
358
    @AddStackToErrorMessage
359
    public async onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) {
1✔
360
        const workspacePaths = (await this.connection.workspace.getWorkspaceFolders()).map(x => util.uriToPath(x.uri));
11✔
361

362
        let changes = params.changes
11✔
363
            .map(x => ({
11✔
364
                srcPath: util.uriToPath(x.uri),
365
                type: x.type,
366
                //if this is an open document, allow this file to be loaded in a standalone project (if applicable)
367
                allowStandaloneProject: this.documents.get(x.uri) !== undefined
368
            }))
369
            //exclude all explicit top-level workspace folder paths (to fix a weird macos fs watcher bug that emits events for the workspace folder itself)
370
            .filter(x => !workspacePaths.includes(x.srcPath));
11✔
371

372
        this.logger.debug('onDidChangeWatchedFiles', changes);
11✔
373

374
        //if the client changed any files containing include/exclude patterns, rebuild the path filterer before processing these changes
375
        if (
11✔
376
            micromatch.some(changes.map(x => x.srcPath), [
10✔
377
                '**/.gitignore',
378
                '**/.vscode/settings.json',
379
                '**/*bsconfig*.json'
380
            ], {
381
                dot: true
382
            })
383
        ) {
384
            await this.rebuildPathFilterer();
7✔
385
        }
386

387
        //handle the file changes
388
        await this.projectManager.handleFileChanges(changes);
11✔
389
    }
390

391
    @AddStackToErrorMessage
392
    private async onDocumentClose(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
1✔
393
        this.logger.debug('onDocumentClose', event.document.uri);
1✔
394

395
        await this.projectManager.handleFileClose({
1✔
396
            srcPath: util.uriToPath(event.document.uri)
397
        });
398
    }
399

400
    /**
401
     * Provide a list of completion items based on the current cursor position
402
     */
403
    @AddStackToErrorMessage
404
    public async onCompletion(params: CompletionParams, cancellationToken: CancellationToken, workDoneProgress: WorkDoneProgressReporter, resultProgress: ResultProgressReporter<CompletionItem[]>): Promise<CompletionList> {
1✔
405
        this.logger.debug('onCompletion', params, cancellationToken);
1✔
406

407
        const srcPath = util.uriToPath(params.textDocument.uri);
1✔
408
        const completions = await this.projectManager.getCompletions({
1✔
409
            srcPath: srcPath,
410
            position: params.position,
411
            cancellationToken: cancellationToken
412
        });
413
        return completions;
1✔
414
    }
415

416
    /**
417
     * Get a list of workspaces, and their configurations.
418
     * Get only the settings for the workspace that are relevant to the language server. We do this so we can cache this object for use in change detection in the future.
419
     */
420
    private async getWorkspaceConfigs(): Promise<WorkspaceConfigWithExtras[]> {
421
        //get all workspace folders (we'll use these to get settings)
422
        let workspaces = await Promise.all(
112✔
423
            (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => {
332!
424
                const workspaceFolder = util.uriToPath(x.uri);
116✔
425
                const brightscriptConfig = await this.getClientConfiguration<BrightScriptClientConfiguration>(x.uri, 'brightscript');
116✔
426
                return {
116✔
427
                    workspaceFolder: workspaceFolder,
428
                    excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
429
                    bsconfigPath: brightscriptConfig.configFile,
430
                    languageServer: {
431
                        enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
696!
432
                        logLevel: brightscriptConfig?.languageServer?.logLevel
696!
433
                    }
434

435
                } as WorkspaceConfigWithExtras;
436
            })
437
        );
438
        return workspaces;
110✔
439
    }
440

441
    private workspaceConfigsCache = new Map<string, WorkspaceConfigWithExtras>();
47✔
442

443
    @AddStackToErrorMessage
444
    public async onDidChangeConfiguration(args: DidChangeConfigurationParams) {
1✔
445
        this.logger.log('onDidChangeConfiguration', 'Reloading all projects');
5✔
446

447
        const configs = new Map(
5✔
448
            (await this.getWorkspaceConfigs()).map(x => [x.workspaceFolder, x])
4✔
449
        );
450
        //find any changed configs. This includes newly created workspaces, deleted workspaces, etc.
451
        //TODO: enhance this to only reload specific projects, depending on the change
452
        if (!isEqual(configs, this.workspaceConfigsCache)) {
5✔
453
            //now that we've processed any config diffs, update the cached copy of them
454
            this.workspaceConfigsCache = configs;
3✔
455

456
            //if configuration changed, rebuild the path filterer
457
            await this.rebuildPathFilterer();
3✔
458

459
            //if the user changes any user/workspace config settings, just mass-reload all projects
460
            await this.syncProjects(true);
3✔
461
        }
462
    }
463

464

465
    @AddStackToErrorMessage
466
    public async onHover(params: TextDocumentPositionParams) {
1✔
UNCOV
467
        this.logger.debug('onHover', params);
×
468

UNCOV
469
        const srcPath = util.uriToPath(params.textDocument.uri);
×
UNCOV
470
        const result = await this.projectManager.getHover({ srcPath: srcPath, position: params.position });
×
UNCOV
471
        return result;
×
472
    }
473

474
    @AddStackToErrorMessage
475
    public async onWorkspaceSymbol(params: WorkspaceSymbolParams) {
1✔
476
        this.logger.debug('onWorkspaceSymbol', params);
4✔
477

478
        const result = await this.projectManager.getWorkspaceSymbol();
4✔
479
        return result;
4✔
480
    }
481

482
    @AddStackToErrorMessage
483
    public async onDocumentSymbol(params: DocumentSymbolParams) {
1✔
484
        this.logger.debug('onDocumentSymbol', params);
6✔
485

486
        const srcPath = util.uriToPath(params.textDocument.uri);
6✔
487
        const result = await this.projectManager.getDocumentSymbol({ srcPath: srcPath });
6✔
488
        return result;
6✔
489
    }
490

491
    @AddStackToErrorMessage
492
    public async onDefinition(params: TextDocumentPositionParams) {
1✔
493
        this.logger.debug('onDefinition', params);
5✔
494

495
        const srcPath = util.uriToPath(params.textDocument.uri);
5✔
496

497
        const result = this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
5✔
498
        return result;
5✔
499
    }
500

501
    @AddStackToErrorMessage
502
    public async onSignatureHelp(params: SignatureHelpParams) {
1✔
503
        this.logger.debug('onSignatureHelp', params);
4✔
504

505
        const srcPath = util.uriToPath(params.textDocument.uri);
4✔
506
        const result = await this.projectManager.getSignatureHelp({ srcPath: srcPath, position: params.position });
4✔
507
        if (result) {
4✔
508
            return result;
3✔
509
        } else {
510
            return {
1✔
511
                signatures: [],
512
                activeSignature: null,
513
                activeParameter: null
514
            };
515
        }
516

517
    }
518

519
    @AddStackToErrorMessage
520
    public async onReferences(params: ReferenceParams) {
1✔
521
        this.logger.debug('onReferences', params);
3✔
522

523
        const srcPath = util.uriToPath(params.textDocument.uri);
3✔
524
        const result = await this.projectManager.getReferences({ srcPath: srcPath, position: params.position });
3✔
525
        return result ?? [];
3!
526
    }
527

528

529
    @AddStackToErrorMessage
530
    private async onFullSemanticTokens(params: SemanticTokensParams) {
1✔
531
        this.logger.debug('onFullSemanticTokens', params);
1✔
532

533
        const srcPath = util.uriToPath(params.textDocument.uri);
1✔
534
        const result = await this.projectManager.getSemanticTokens({ srcPath: srcPath });
1✔
535

536
        return {
1✔
537
            data: encodeSemanticTokens(result)
538
        } as SemanticTokens;
539
    }
540

541
    @AddStackToErrorMessage
542
    public async onCodeAction(params: CodeActionParams) {
1✔
UNCOV
543
        this.logger.debug('onCodeAction', params);
×
544

UNCOV
545
        const srcPath = util.uriToPath(params.textDocument.uri);
×
546
        const result = await this.projectManager.getCodeActions({ srcPath: srcPath, range: params.range });
×
UNCOV
547
        return result;
×
548
    }
549

550

551
    @AddStackToErrorMessage
552
    public async onExecuteCommand(params: ExecuteCommandParams) {
1✔
553
        this.logger.debug('onExecuteCommand', params);
2✔
554

555
        if (params.command === CustomCommands.TranspileFile) {
2!
556
            const args = {
2✔
557
                srcPath: params.arguments[0] as string
558
            };
559
            const result = await this.projectManager.transpileFile(args);
2✔
560
            //back-compat: include `pathAbsolute` property so older vscode versions still work
561
            (result as any).pathAbsolute = result.srcPath;
2✔
562
            return result;
2✔
563
        }
564
    }
565

566
    /**
567
     * Establish a connection to the client if not already connected
568
     */
569
    private establishConnection() {
570
        if (!this.connection) {
×
UNCOV
571
            this.connection = createConnection(ProposedFeatures.all);
×
572
        }
UNCOV
573
        return this.connection;
×
574
    }
575

576
    /**
577
     * Send a new busy status notification to the client based on the current busy status
578
     */
579
    private sendBusyStatus() {
580
        this.busyStatusIndex = ++this.busyStatusIndex <= 0 ? 0 : this.busyStatusIndex;
222✔
581

582
        this.connection.sendNotification(NotificationName.busyStatus, {
222!
583
            status: this.projectManager.busyStatusTracker.status,
584
            timestamp: Date.now(),
585
            index: this.busyStatusIndex,
586
            activeRuns: [...this.projectManager.busyStatusTracker.activeRuns]
587
        })?.catch(logAndIgnoreError);
222!
588
    }
589
    private busyStatusIndex = -1;
47✔
590

591
    /**
592
     * Populate the path filterer with the client's include/exclude lists and the projects include lists
593
     * @returns the instance of the path filterer
594
     */
595
    private async rebuildPathFilterer() {
596
        this.pathFilterer.clear();
8✔
597
        const workspaceFolders = await this.connection.workspace.getWorkspaceFolders();
8✔
598
        await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
8✔
599
            const rootDir = util.uriToPath(workspaceFolder.uri);
8✔
600

601
            //always exclude everything from these common folders
602
            this.pathFilterer.registerExcludeList(rootDir, [
8✔
603
                '**/node_modules/**/*',
604
                '**/.git/**/*',
605
                'out/**/*',
606
                '**/.roku-deploy-staging/**/*'
607
            ]);
608
            //get any `files.exclude` patterns from the client from this workspace
609
            const workspaceExcludeGlobs = await this.getWorkspaceExcludeGlobs(rootDir);
8✔
610
            this.pathFilterer.registerExcludeList(rootDir, workspaceExcludeGlobs);
8✔
611

612
            //get any .gitignore patterns from the client from this workspace
613
            const gitignorePath = path.resolve(rootDir, '.gitignore');
8✔
614
            if (await fsExtra.pathExists(gitignorePath)) {
8!
UNCOV
615
                const matcher = ignore().add(
×
616
                    fsExtra.readFileSync(gitignorePath).toString()
617
                );
UNCOV
618
                this.pathFilterer.registerExcludeMatcher((path: string) => {
×
UNCOV
619
                    return matcher.test(path).ignored;
×
620
                });
621
            }
622
        }));
623
        this.logger.log('pathFilterer successfully reconstructed');
8✔
624

625
        return this.pathFilterer;
8✔
626
    }
627

628
    /**
629
     * Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
630
     */
631
    private async getWorkspaceExcludeGlobs(workspaceFolder: string): Promise<string[]> {
632
        const config = await this.getClientConfiguration<{ exclude: string[] }>(workspaceFolder, 'files');
124✔
633
        const result = Object
124✔
634
            .keys(config?.exclude ?? {})
744✔
635
            .filter(x => config?.exclude?.[x])
8!
636
            //vscode files.exclude patterns support ignoring folders without needing to add `**/*`. So for our purposes, we need to
637
            //append **/* to everything without a file extension or magic at the end
638
            .map(pattern => [
8✔
639
                //send the pattern as-is (this handles weird cases and exact file matches)
640
                pattern,
641
                //treat the pattern as a directory (no harm in doing this because if it's a file, the pattern will just never match anything)
642
                `${pattern}/**/*`
643
            ])
644
            .flat(1);
645
        return result;
124✔
646
    }
647

648
    /**
649
     * Ask the project manager to sync all projects found within the list of workspaces
650
     * @param forceReload if true, all projects are discarded and recreated from scratch
651
     */
652
    private async syncProjects(forceReload = false) {
31✔
653
        const workspaces = await this.getWorkspaceConfigs();
31✔
654

655
        await this.projectManager.syncProjects(workspaces, forceReload);
31✔
656

657
        //set our logLevel to the most verbose log level found across all projects and workspaces
658
        await this.syncLogLevel();
31✔
659
    }
660

661
    /**
662
     * Given a workspaceFolder path, get the specified configuration from the client (if applicable).
663
     * Be sure to use optional chaining to traverse the result in case that configuration doesn't exist or the client doesn't support `getConfiguration`
664
     * @param workspaceFolder the folder for the workspace in the client
665
     */
666
    private async getClientConfiguration<T extends Record<string, any>>(workspaceFolder: string, section: string): Promise<T> {
667
        const scopeUri = util.pathToUri(workspaceFolder);
230✔
668
        let config = {};
230✔
669

670
        //if the client supports configuration, look for config group called "brightscript"
671
        if (this.hasConfigurationCapability) {
230✔
672
            config = await this.connection.workspace.getConfiguration({
218✔
673
                scopeUri: scopeUri,
674
                section: section
675
            });
676
        }
677
        return config as T;
230✔
678
    }
679

680
    /**
681
     * Send a critical failure notification to the client, which should show a notification of some kind
682
     */
683
    private sendCriticalFailure(message: string) {
UNCOV
684
        this.connection.sendNotification('critical-failure', message).catch(logAndIgnoreError);
×
685
    }
686

687
    /**
688
     * Send diagnostics to the client
689
     */
690
    private async sendDiagnostics(options: { project: LspProject; diagnostics: LspDiagnostic[] }) {
691
        const patch = this.diagnosticCollection.getPatch(options.project.projectNumber, options.diagnostics);
48✔
692

693
        await Promise.all(Object.keys(patch).map(async (srcPath) => {
48✔
694
            const uri = URI.file(srcPath).toString();
17✔
695
            const diagnostics = patch[srcPath].map(d => util.toDiagnostic(d, uri));
17✔
696

697
            await this.connection.sendDiagnostics({
17✔
698
                uri: uri,
699
                diagnostics: diagnostics
700
            });
701
        }));
702
    }
703
    private diagnosticCollection = new DiagnosticCollection();
47✔
704

705
    protected dispose() {
706
        this.loggerSubscription?.();
47✔
707
        this.projectManager?.dispose?.();
47!
708
    }
709
}
710

711
export enum CustomCommands {
1✔
712
    TranspileFile = 'TranspileFile'
1✔
713
}
714

715
export enum NotificationName {
1✔
716
    busyStatus = 'busyStatus'
1✔
717
}
718

719
/**
720
 * Wraps a method. If there's an error (either sync or via a promise),
721
 * this appends the error's stack trace at the end of the error message so that the connection will
722
 */
723
function AddStackToErrorMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
724
    let originalMethod = descriptor.value;
16✔
725

726
    //wrapping the original method
727
    descriptor.value = function value(...args: any[]) {
16✔
728
        try {
89✔
729
            let result = originalMethod.apply(this, args);
89✔
730
            //if the result looks like a promise, log if there's a rejection
731
            if (result?.then) {
89!
732
                return Promise.resolve(result).catch((e: Error) => {
88✔
UNCOV
733
                    if (e?.stack) {
×
UNCOV
734
                        e.message = e.stack;
×
735
                    }
UNCOV
736
                    return Promise.reject(e);
×
737
                });
738
            } else {
739
                return result;
1✔
740
            }
741
        } catch (e: any) {
742
            if (e?.stack) {
×
743
                e.message = e.stack;
×
744
            }
UNCOV
745
            throw e;
×
746
        }
747
    };
748
}
749

750
type Handler<T> = {
751
    [K in keyof T as K extends `on${string}` ? K : never]:
752
    T[K] extends (arg: infer U) => void ? (arg: U) => void : never;
753
};
754
// Extracts the argument type from the function and constructs the desired interface
755
export type OnHandler<T> = {
756
    [K in keyof Handler<T>]: Handler<T>[K] extends (arg: infer U) => void ? U : never;
757
};
758

759
interface BrightScriptClientConfiguration {
760
    configFile: string;
761
    languageServer: {
762
        enableThreading: boolean;
763
        logLevel: LogLevel | string;
764
    };
765
}
766

767
function logAndIgnoreError(error: Error) {
768
    if (error?.stack) {
2!
769
        error.message = error.stack;
2✔
770
    }
771
    console.error(error);
2✔
772
}
773

774
export type WorkspaceConfigWithExtras = WorkspaceConfig & {
775
    bsconfigPath: string;
776
    languageServer: {
777
        enableThreading: boolean;
778
        logLevel: LogLevel | string | undefined;
779
    };
780
};
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