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

rokucommunity / brighterscript / #13172

11 Oct 2024 08:05PM UTC coverage: 89.045%. Remained the same
#13172

push

web-flow
Merge f0820ca5e into 9407565e3

7209 of 8534 branches covered (84.47%)

Branch coverage included in aggregate %.

11 of 11 new or added lines in 2 files covered. (100.0%)

8 existing lines in 2 files now uncovered.

9617 of 10362 relevant lines covered (92.81%)

1781.36 hits per line

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

86.1
/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;
52✔
68

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

74
    private hasConfigurationCapability = false;
52✔
75

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

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

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

106
        this.projectManager = new ProjectManager({
52✔
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) => {
52✔
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) => {
52✔
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) => {
52✔
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✔
266
                    await this.syncProjects();
×
267
                });
268
            }
269
        } catch (e: any) {
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>(
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(
117✔
423
            (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => {
347!
424
                const workspaceFolder = util.uriToPath(x.uri);
121✔
425
                const brightscriptConfig = await this.getClientConfiguration<BrightScriptClientConfiguration>(x.uri, 'brightscript');
121✔
426
                return {
121✔
427
                    workspaceFolder: workspaceFolder,
428
                    excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
429
                    bsconfigPath: brightscriptConfig.configFile,
430
                    languageServer: {
431
                        enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
726!
432
                        logLevel: brightscriptConfig?.languageServer?.logLevel
726!
433
                    }
434

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

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

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

545
        const srcPath = util.uriToPath(params.textDocument.uri);
×
546
        const result = await this.projectManager.getCodeActions({ srcPath: srcPath, range: params.range });
×
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) {
×
571
            this.connection = createConnection(ProposedFeatures.all);
×
572
        }
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;
52✔
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();
13✔
597
        const workspaceConfigs = await this.getWorkspaceConfigs();
13✔
598
        await Promise.all(workspaceConfigs.map(async (workspaceConfig) => {
13✔
599
            const rootDir = util.uriToPath(workspaceConfig.workspaceFolder);
14✔
600

601
            //always exclude everything from these common folders
602
            this.pathFilterer.registerExcludeList(rootDir, [
14✔
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
            this.pathFilterer.registerExcludeList(rootDir, workspaceConfig.excludePatterns);
14✔
610

611
            //get any .gitignore patterns from the client from this workspace
612
            const gitignorePath = path.resolve(rootDir, '.gitignore');
14✔
613
            if (await fsExtra.pathExists(gitignorePath)) {
14✔
614
                const matcher = ignore({ ignoreCase: true }).add(
4✔
615
                    fsExtra.readFileSync(gitignorePath).toString()
616
                );
617
                this.pathFilterer.registerExcludeMatcher((p: string) => {
4✔
618
                    const relPath = path.relative(rootDir, p);
8✔
619
                    if (ignore.isPathValid(relPath)) {
8✔
620
                        return matcher.test(relPath).ignored;
5✔
621
                    } else {
622
                        //we do not have a valid relative path, so we cannot determine if it is ignored...thus it is NOT ignored
623
                        return false;
3✔
624
                    }
625
                });
626
            }
627
        }));
628
        this.logger.log('pathFilterer successfully reconstructed');
13✔
629

630
        return this.pathFilterer;
13✔
631
    }
632

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

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

660
        await this.projectManager.syncProjects(workspaces, forceReload);
31✔
661

662
        //set our logLevel to the most verbose log level found across all projects and workspaces
663
        await this.syncLogLevel();
31✔
664
    }
665

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

675
        //if the client supports configuration, look for config group called "brightscript"
676
        if (this.hasConfigurationCapability) {
232✔
677
            config = await this.connection.workspace.getConfiguration({
219✔
678
                scopeUri: scopeUri,
679
                section: section
680
            });
681
        }
682
        return config as T;
232✔
683
    }
684

685
    /**
686
     * Send a critical failure notification to the client, which should show a notification of some kind
687
     */
688
    private sendCriticalFailure(message: string) {
UNCOV
689
        this.connection.sendNotification('critical-failure', message).catch(logAndIgnoreError);
×
690
    }
691

692
    /**
693
     * Send diagnostics to the client
694
     */
695
    private async sendDiagnostics(options: { project: LspProject; diagnostics: LspDiagnostic[] }) {
696
        const patch = this.diagnosticCollection.getPatch(options.project.projectNumber, options.diagnostics);
48✔
697

698
        await Promise.all(Object.keys(patch).map(async (srcPath) => {
48✔
699
            const uri = URI.file(srcPath).toString();
17✔
700
            const diagnostics = patch[srcPath].map(d => util.toDiagnostic(d, uri));
17✔
701

702
            await this.connection.sendDiagnostics({
17✔
703
                uri: uri,
704
                diagnostics: diagnostics
705
            });
706
        }));
707
    }
708
    private diagnosticCollection = new DiagnosticCollection();
52✔
709

710
    protected dispose() {
711
        this.loggerSubscription?.();
52✔
712
        this.projectManager?.dispose?.();
52!
713
    }
714
}
715

716
export enum CustomCommands {
1✔
717
    TranspileFile = 'TranspileFile'
1✔
718
}
719

720
export enum NotificationName {
1✔
721
    busyStatus = 'busyStatus'
1✔
722
}
723

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

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

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

764
interface BrightScriptClientConfiguration {
765
    configFile: string;
766
    languageServer: {
767
        enableThreading: boolean;
768
        logLevel: LogLevel | string;
769
    };
770
}
771

772
function logAndIgnoreError(error: Error) {
773
    if (error?.stack) {
2!
774
        error.message = error.stack;
2✔
775
    }
776
    console.error(error);
2✔
777
}
778

779
export type WorkspaceConfigWithExtras = WorkspaceConfig & {
780
    bsconfigPath: string;
781
    languageServer: {
782
        enableThreading: boolean;
783
        logLevel: LogLevel | string | undefined;
784
    };
785
};
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