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

rokucommunity / brighterscript / #13239

28 Oct 2024 07:44PM UTC coverage: 89.066% (+0.9%) from 88.214%
#13239

push

web-flow
Merge 2aeae9b6e into 7cfaaa047

7233 of 8558 branches covered (84.52%)

Branch coverage included in aggregate %.

1076 of 1197 new or added lines in 28 files covered. (89.89%)

24 existing lines in 5 files now uncovered.

9621 of 10365 relevant lines covered (92.82%)

1782.54 hits per line

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

86.27
/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;
54✔
68

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

74
    private hasConfigurationCapability = false;
54✔
75

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

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

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

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

125
            //resend all open document changes
126
            const documents = [...this.documents.all()];
40✔
127
            if (documents.length > 0) {
40✔
128
                this.logger.log(`[${event.project?.projectIdentifier}] 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('active-runs-change', (event) => {
54✔
138
            console.log(event);
398✔
139
            this.sendBusyStatus();
398✔
140
        });
141
    }
142

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

262
            await this.syncProjects();
3✔
263

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

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

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

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

320
        const workspaces = await this.getWorkspaceConfigs();
82✔
321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

417
    /**
418
     * Get a list of workspaces, and their configurations.
419
     * 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.
420
     */
421
    private async getWorkspaceConfigs(): Promise<WorkspaceConfigWithExtras[]> {
422
        //get all workspace folders (we'll use these to get settings)
423
        let workspaces = await Promise.all(
117✔
424
            (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => {
347!
425
                const workspaceFolder = util.uriToPath(x.uri);
121✔
426
                const brightscriptConfig = await this.getClientConfiguration<BrightScriptClientConfiguration>(x.uri, 'brightscript');
121✔
427
                return {
121✔
428
                    workspaceFolder: workspaceFolder,
429
                    excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
430
                    bsconfigPath: brightscriptConfig.configFile,
431
                    languageServer: {
432
                        enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
726!
433
                        logLevel: brightscriptConfig?.languageServer?.logLevel
726!
434
                    }
435

436
                } as WorkspaceConfigWithExtras;
437
            })
438
        );
439
        return workspaces;
115✔
440
    }
441

442
    private workspaceConfigsCache = new Map<string, WorkspaceConfigWithExtras>();
54✔
443

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

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

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

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

465

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

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

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

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

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

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

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

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

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

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

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

518
    }
519

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

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

529

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

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

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

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

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

551

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

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

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

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

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

599
    private pathFiltererDisposables: Array<() => void> = [];
54✔
600

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

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

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

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

650
        return this.pathFilterer;
16✔
651
    }
652

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

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

680
        await this.projectManager.syncProjects(workspaces, forceReload);
33✔
681

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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