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

rokucommunity / brighterscript / #13315

22 Nov 2024 10:12PM UTC coverage: 89.121% (+0.9%) from 88.237%
#13315

push

web-flow
Merge fea67a504 into f6dedf73f

7359 of 8706 branches covered (84.53%)

Branch coverage included in aggregate %.

1121 of 1237 new or added lines in 29 files covered. (90.62%)

24 existing lines in 5 files now uncovered.

9722 of 10460 relevant lines covered (92.94%)

1825.2 hits per line

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

86.23
/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;
55✔
68

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

74
    private hasConfigurationCapability = false;
55✔
75

76
    /**
77
     * Indicates whether the client supports workspace folders
78
     */
79
    private clientHasWorkspaceFolderCapability = false;
55✔
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);
55✔
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({
55✔
96
        logLevel: LogLevel.log
97
    });
98

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

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

106
        this.projectManager = new ProjectManager({
55✔
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) => {
55✔
113
            this.logger.debug(`Received ${event.diagnostics.length} diagnostics from project ${event.project.projectNumber}`);
56✔
114
            this.sendDiagnostics(event).catch(logAndIgnoreError);
56✔
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) => {
55✔
122
            //keep logLevel in sync with the most verbose log level found across all projects
123
            this.syncLogLevel().catch(logAndIgnoreError);
43✔
124

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

137
        this.projectManager.busyStatusTracker.on('active-runs-change', (event) => {
55✔
138
            this.sendBusyStatus();
424✔
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();
18✔
146

147
        //disable logger colors when running in LSP mode
148
        logger.enableColor = false;
18✔
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) => {
18✔
152
            this.connection.tracer.log(message.argsText);
150✔
153
        });
154

155
        //bind all our on* methods that share the same name from connection
156
        for (const name of Object.getOwnPropertyNames(LanguageServer.prototype)) {
18✔
157
            if (/on+/.test(name) && typeof this.connection?.[name] === 'function') {
522!
158
                this.connection[name](this[name].bind(this));
252✔
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));
18✔
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));
18✔
170

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

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

177
        // Listen on the connection
178
        this.connection.listen();
18✔
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✔
266
                    await this.syncProjects();
×
267
                });
268
            }
269
        } catch (e: any) {
NEW
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
            );
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>(
86✔
288
            items: T[],
289
            fetcher: (item: T) => MaybePromise<LogLevel | string>
290
        ): Promise<{ logLevel: LogLevel; logLevelText: string; item: T }> => {
291
            const logLevels = await Promise.all(
161✔
292
                items.map(async (item) => {
293
                    let value = await fetcher(item);
179✔
294
                    //force string values to lower case (so we can support things like 'log' or 'Log' or 'LOG')
295
                    if (typeof value === 'string') {
179✔
296
                        value = value.toLowerCase();
14✔
297
                    }
298
                    const logLevelNumeric = this.logger.getLogLevelNumeric(value as any);
179✔
299

300
                    if (typeof logLevelNumeric === 'number') {
179✔
301
                        return logLevelNumeric;
98✔
302
                    } else {
303
                        return -1;
81✔
304
                    }
305
                })
306
            );
307
            let idx = logLevels.findIndex(x => x > -1);
161✔
308
            if (idx > -1) {
161✔
309
                const mostVerboseLogLevel = Math.max(...logLevels);
79✔
310
                return {
79✔
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)]
80✔
315
                };
316
            }
317
        };
318

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

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

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

329
        let projectResult = await getLogLevel(this.projectManager.projects, (project) => project.logger.logLevel);
90✔
330
        if (projectResult) {
77✔
331
            this.logger.info(`Setting global logLevel to '${projectResult.logLevelText}' based on project #${projectResult?.item?.projectNumber}`);
72!
332
            this.logger.logLevel = projectResult.logLevel;
72✔
333
            return;
72✔
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);
49✔
343

344
        await this.projectManager.handleFileChanges([{
49✔
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(
122✔
423
            (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => {
362!
424
                const workspaceFolder = util.uriToPath(x.uri);
126✔
425
                const brightscriptConfig = await this.getClientConfiguration<BrightScriptClientConfiguration>(x.uri, 'brightscript');
126✔
426
                return {
126✔
427
                    workspaceFolder: workspaceFolder,
428
                    excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
429
                    bsconfigPath: brightscriptConfig.configFile,
430
                    languageServer: {
431
                        enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
756!
432
                        logLevel: brightscriptConfig?.languageServer?.logLevel
756!
433
                    }
434

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

441
    private workspaceConfigsCache = new Map<string, WorkspaceConfigWithExtras>();
55✔
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✔
NEW
467
        this.logger.debug('onHover', params);
×
468

NEW
469
        const srcPath = util.uriToPath(params.textDocument.uri);
×
NEW
470
        const result = await this.projectManager.getHover({ srcPath: srcPath, position: params.position });
×
NEW
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✔
NEW
543
        this.logger.debug('onCodeAction', params);
×
544

NEW
545
        const srcPath = util.uriToPath(params.textDocument.uri);
×
NEW
546
        const result = await this.projectManager.getCodeActions({ srcPath: srcPath, range: params.range });
×
NEW
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() {
NEW
570
        if (!this.connection) {
×
NEW
571
            this.connection = createConnection(ProposedFeatures.all);
×
572
        }
NEW
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;
424✔
581

582
        this.connection.sendNotification(NotificationName.busyStatus, {
424!
583
            status: this.projectManager.busyStatusTracker.status,
584
            timestamp: Date.now(),
585
            index: this.busyStatusIndex,
586
            activeRuns: [
587
                //extract only specific information from the active run so we know what's going on
588
                ...this.projectManager.busyStatusTracker.activeRuns.map(x => ({
683✔
589
                    scope: x.scope?.projectIdentifier,
2,049✔
590
                    label: x.label,
591
                    startTime: x.startTime.getTime()
592
                }))
593
            ]
594
        })?.catch(logAndIgnoreError);
424!
595
    }
596
    private busyStatusIndex = -1;
55✔
597

598
    private pathFiltererDisposables: Array<() => void> = [];
55✔
599

600
    /**
601
     * Populate the path filterer with the client's include/exclude lists and the projects include lists
602
     * @returns the instance of the path filterer
603
     */
604
    private async rebuildPathFilterer() {
605
        //dispose of any previous pathFilterer disposables
606
        this.pathFiltererDisposables?.forEach(dispose => dispose());
16!
607
        //keep track of all the pathFilterer disposables so we can dispose them later
608
        this.pathFiltererDisposables = [];
16✔
609

610
        const workspaceConfigs = await this.getWorkspaceConfigs();
16✔
611
        await Promise.all(workspaceConfigs.map(async (workspaceConfig) => {
16✔
612
            const rootDir = util.uriToPath(workspaceConfig.workspaceFolder);
17✔
613

614
            //always exclude everything from these common folders
615
            this.pathFiltererDisposables.push(
17✔
616
                this.pathFilterer.registerExcludeList(rootDir, [
617
                    '**/node_modules/**/*',
618
                    '**/.git/**/*',
619
                    'out/**/*',
620
                    '**/.roku-deploy-staging/**/*'
621
                ])
622
            );
623
            //get any `files.exclude` patterns from the client from this workspace
624
            this.pathFiltererDisposables.push(
17✔
625
                this.pathFilterer.registerExcludeList(rootDir, workspaceConfig.excludePatterns)
626
            );
627

628
            //get any .gitignore patterns from the client from this workspace
629
            const gitignorePath = path.resolve(rootDir, '.gitignore');
17✔
630
            if (await fsExtra.pathExists(gitignorePath)) {
17✔
631
                const matcher = ignore({ ignoreCase: true }).add(
4✔
632
                    fsExtra.readFileSync(gitignorePath).toString()
633
                );
634
                this.pathFiltererDisposables.push(
4✔
635
                    this.pathFilterer.registerExcludeMatcher((p: string) => {
636
                        const relPath = path.relative(rootDir, p);
8✔
637
                        if (ignore.isPathValid(relPath)) {
8✔
638
                            return matcher.test(relPath).ignored;
5✔
639
                        } else {
640
                            //we do not have a valid relative path, so we cannot determine if it is ignored...thus it is NOT ignored
641
                            return false;
3✔
642
                        }
643
                    })
644
                );
645
            }
646
        }));
647
        this.logger.log('pathFilterer successfully reconstructed');
16✔
648

649
        return this.pathFilterer;
16✔
650
    }
651

652
    /**
653
     * Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
654
     */
655
    private async getWorkspaceExcludeGlobs(workspaceFolder: string): Promise<string[]> {
656
        const config = await this.getClientConfiguration<{ exclude: string[] }>(workspaceFolder, 'files');
126✔
657
        const result = Object
126✔
658
            .keys(config?.exclude ?? {})
756✔
659
            .filter(x => config?.exclude?.[x])
8!
660
            //vscode files.exclude patterns support ignoring folders without needing to add `**/*`. So for our purposes, we need to
661
            //append **/* to everything without a file extension or magic at the end
662
            .map(pattern => [
8✔
663
                //send the pattern as-is (this handles weird cases and exact file matches)
664
                pattern,
665
                //treat the pattern as a directory (no harm in doing this because if it's a file, the pattern will just never match anything)
666
                `${pattern}/**/*`
667
            ])
668
            .flat(1);
669
        return result;
126✔
670
    }
671

672
    /**
673
     * Ask the project manager to sync all projects found within the list of workspaces
674
     * @param forceReload if true, all projects are discarded and recreated from scratch
675
     */
676
    private async syncProjects(forceReload = false) {
34✔
677
        const workspaces = await this.getWorkspaceConfigs();
34✔
678

679
        await this.projectManager.syncProjects(workspaces, forceReload);
34✔
680

681
        //set our logLevel to the most verbose log level found across all projects and workspaces
682
        await this.syncLogLevel();
34✔
683
    }
684

685
    /**
686
     * Given a workspaceFolder path, get the specified configuration from the client (if applicable).
687
     * Be sure to use optional chaining to traverse the result in case that configuration doesn't exist or the client doesn't support `getConfiguration`
688
     * @param workspaceFolder the folder for the workspace in the client
689
     */
690
    private async getClientConfiguration<T extends Record<string, any>>(workspaceFolder: string, section: string): Promise<T> {
691
        const scopeUri = util.pathToUri(workspaceFolder);
242✔
692
        let config = {};
242✔
693

694
        //if the client supports configuration, look for config group called "brightscript"
695
        if (this.hasConfigurationCapability) {
242✔
696
            config = await this.connection.workspace.getConfiguration({
229✔
697
                scopeUri: scopeUri,
698
                section: section
699
            });
700
        }
701
        return config as T;
242✔
702
    }
703

704
    /**
705
     * Send a critical failure notification to the client, which should show a notification of some kind
706
     */
707
    private sendCriticalFailure(message: string) {
NEW
708
        this.connection.sendNotification('critical-failure', message).catch(logAndIgnoreError);
×
709
    }
710

711
    /**
712
     * Send diagnostics to the client
713
     */
714
    private async sendDiagnostics(options: { project: LspProject; diagnostics: LspDiagnostic[] }) {
715
        const patch = this.diagnosticCollection.getPatch(options.project.projectNumber, options.diagnostics);
56✔
716

717
        await Promise.all(Object.keys(patch).map(async (srcPath) => {
56✔
718
            const uri = URI.file(srcPath).toString();
17✔
719
            const diagnostics = patch[srcPath].map(d => util.toDiagnostic(d, uri));
17✔
720

721
            await this.connection.sendDiagnostics({
17✔
722
                uri: uri,
723
                diagnostics: diagnostics
724
            });
725
        }));
726
    }
727
    private diagnosticCollection = new DiagnosticCollection();
55✔
728

729
    protected dispose() {
730
        this.loggerSubscription?.();
55✔
731
        this.projectManager?.dispose?.();
55!
732
    }
733
}
734

735
export enum CustomCommands {
1✔
736
    TranspileFile = 'TranspileFile'
1✔
737
}
738

739
export enum NotificationName {
1✔
740
    busyStatus = 'busyStatus'
1✔
741
}
742

743
/**
744
 * Wraps a method. If there's an error (either sync or via a promise),
745
 * this appends the error's stack trace at the end of the error message so that the connection will
746
 */
747
function AddStackToErrorMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
748
    let originalMethod = descriptor.value;
16✔
749

750
    //wrapping the original method
751
    descriptor.value = function value(...args: any[]) {
16✔
752
        try {
96✔
753
            let result = originalMethod.apply(this, args);
96✔
754
            //if the result looks like a promise, log if there's a rejection
755
            if (result?.then) {
96!
756
                return Promise.resolve(result).catch((e: Error) => {
95✔
757
                    if (e?.stack) {
×
758
                        e.message = e.stack;
×
759
                    }
760
                    return Promise.reject(e);
×
761
                });
762
            } else {
763
                return result;
1✔
764
            }
765
        } catch (e: any) {
766
            if (e?.stack) {
×
767
                e.message = e.stack;
×
768
            }
769
            throw e;
×
770
        }
771
    };
772
}
773

774
type Handler<T> = {
775
    [K in keyof T as K extends `on${string}` ? K : never]:
776
    T[K] extends (arg: infer U) => void ? (arg: U) => void : never;
777
};
778
// Extracts the argument type from the function and constructs the desired interface
779
export type OnHandler<T> = {
780
    [K in keyof Handler<T>]: Handler<T>[K] extends (arg: infer U) => void ? U : never;
781
};
782

783
interface BrightScriptClientConfiguration {
784
    configFile: string;
785
    languageServer: {
786
        enableThreading: boolean;
787
        logLevel: LogLevel | string;
788
    };
789
}
790

791
function logAndIgnoreError(error: Error) {
792
    if (error?.stack) {
2!
793
        error.message = error.stack;
2✔
794
    }
795
    console.error(error);
2✔
796
}
797

798
export type WorkspaceConfigWithExtras = WorkspaceConfig & {
799
    bsconfigPath: string;
800
    languageServer: {
801
        enableThreading: boolean;
802
        logLevel: LogLevel | string | undefined;
803
    };
804
};
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