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

rokucommunity / brighterscript / #13061

23 Sep 2024 02:51PM UTC coverage: 88.769% (+0.8%) from 87.933%
#13061

push

web-flow
Merge 373852d93 into 56dcaaa63

6620 of 7920 branches covered (83.59%)

Branch coverage included in aggregate %.

1025 of 1156 new or added lines in 28 files covered. (88.67%)

24 existing lines in 5 files now uncovered.

9456 of 10190 relevant lines covered (92.8%)

1711.78 hits per line

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

83.46
/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

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

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

72
    private hasConfigurationCapability = false;
41✔
73

74
    /**
75
     * Indicates whether the client supports workspace folders
76
     */
77
    private clientHasWorkspaceFolderCapability = false;
41✔
78

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

85
    private loggerSubscription: () => void;
86

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

93
    public logger = createLogger({
41✔
94
        logLevel: LogLevel.log
95
    });
96

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

102
        this.pathFilterer = new PathFilterer({ logger: this.logger });
41✔
103

104
        this.projectManager = new ProjectManager({
41✔
105
            pathFilterer: this.pathFilterer,
106
            logger: this.logger.createLogger()
107
        });
108

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

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

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

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

135
        this.projectManager.busyStatusTracker.on('change', (event) => {
41✔
136
            this.sendBusyStatus();
222✔
137
        });
138
    }
139

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

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

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

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

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

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

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

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

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

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

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

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

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

233
        //set our logger to the most verbose logLevel found across any project
234
        await this.syncLogLevel();
3✔
235

236
        try {
3✔
237
            if (this.hasConfigurationCapability) {
3✔
238
                // register for when the user changes workspace or user settings
239
                await this.connection.client.register(
2✔
240
                    DidChangeConfigurationNotification.type,
241
                    {
242
                        //we only care about when these settings sections change
243
                        section: [
244
                            'brightscript',
245
                            'files'
246
                        ]
247
                    } as DidChangeConfigurationRegistrationOptions
248
                );
249
            }
250

251
            //populate the path filterer with the client's include/exclude lists
252
            await this.rebuildPathFilterer();
3✔
253

254
            await this.syncProjects();
3✔
255

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

267
                Error message: ${e.message}`
268
            );
269
            throw e;
×
270
        }
271
    }
272

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

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

312
        let workspaceResult = await getLogLevel(
78✔
313
            await this.connection.workspace.getWorkspaceFolders(),
314
            async (workspace) => {
315
                const config = (await this.getClientConfiguration<BrightScriptClientConfiguration>(workspace.uri, 'brightscript'));
81✔
316
                return config?.languageServer?.logLevel;
81!
317
            }
318
        );
319
        if (workspaceResult) {
76✔
320
            this.logger.info(`Setting global logLevel to '${workspaceResult.logLevelText}' based on configuration from workspace '${workspaceResult?.item?.uri}'`);
3!
321
            this.logger.logLevel = workspaceResult.logLevel;
3✔
322
            return;
3✔
323
        }
324

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

332
        //use a default level if no other level was found
333
        this.logger.logLevel = LogLevel.log;
5✔
334
    }
335

336
    @AddStackToErrorMessage
337
    private async onTextDocumentDidChangeContent(event: TextDocumentChangeEvent<TextDocument>) {
1✔
338
        this.logger.debug('onTextDocumentDidChangeContent', event.document.uri);
42✔
339

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

348
    /**
349
     * Called when watched files changed (add/change/delete).
350
     * The CLIENT is in charge of what files to watch, so all client
351
     * implementations should ensure that all valid project
352
     * file types are watched (.brs,.bs,.xml,manifest, and any json/text/image files)
353
     */
354
    @AddStackToErrorMessage
355
    public async onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) {
1✔
356
        const changes = params.changes.map(x => ({
10✔
357
            srcPath: util.uriToPath(x.uri),
358
            type: x.type,
359
            //if this is an open document, allow this file to be loaded in a standalone project (if applicable)
360
            allowStandaloneProject: this.documents.get(x.uri) !== undefined
361
        }));
362

363
        this.logger.debug('onDidChangeWatchedFiles', changes);
10✔
364

365
        //if the client changed any files containing include/exclude patterns, rebuild the path filterer before processing these changes
366
        if (
10✔
367
            micromatch.some(changes.map(x => x.srcPath), [
10✔
368
                '**/.gitignore',
369
                '**/.vscode/settings.json',
370
                '**/*bsconfig*.json'
371
            ], {
372
                dot: true
373
            })
374
        ) {
375
            await this.rebuildPathFilterer();
7✔
376
        }
377

378
        //handle the file changes
379
        await this.projectManager.handleFileChanges(changes);
10✔
380
    }
381

382
    @AddStackToErrorMessage
383
    private async onDocumentClose(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
1✔
384
        this.logger.debug('onDocumentClose', event.document.uri);
1✔
385

386
        await this.projectManager.handleFileClose({
1✔
387
            srcPath: util.uriToPath(event.document.uri)
388
        });
389
    }
390

391
    /**
392
     * Provide a list of completion items based on the current cursor position
393
     */
394
    @AddStackToErrorMessage
395
    public async onCompletion(params: CompletionParams, cancellationToken: CancellationToken, workDoneProgress: WorkDoneProgressReporter, resultProgress: ResultProgressReporter<CompletionItem[]>): Promise<CompletionList> {
1✔
396
        this.logger.debug('onCompletion', params, cancellationToken);
1✔
397

398
        const srcPath = util.uriToPath(params.textDocument.uri);
1✔
399
        const completions = await this.projectManager.getCompletions({
1✔
400
            srcPath: srcPath,
401
            position: params.position,
402
            cancellationToken: cancellationToken
403
        });
404
        return completions;
1✔
405
    }
406

407
    @AddStackToErrorMessage
408
    public async onDidChangeConfiguration(args: DidChangeConfigurationParams) {
1✔
NEW
409
        this.logger.log('onDidChangeConfiguration', 'Reloading all projects');
×
410

411
        //if configuration changed, rebuild the path filterer
NEW
412
        await this.rebuildPathFilterer();
×
413

414
        //if the user changes any user/workspace config settings, just mass-reload all projects
NEW
415
        await this.syncProjects(true);
×
416
    }
417

418
    @AddStackToErrorMessage
419
    public async onHover(params: TextDocumentPositionParams) {
1✔
NEW
420
        this.logger.debug('onHover', params);
×
421

NEW
422
        const srcPath = util.uriToPath(params.textDocument.uri);
×
NEW
423
        const result = await this.projectManager.getHover({ srcPath: srcPath, position: params.position });
×
NEW
424
        return result;
×
425
    }
426

427
    @AddStackToErrorMessage
428
    public async onWorkspaceSymbol(params: WorkspaceSymbolParams) {
1✔
429
        this.logger.debug('onWorkspaceSymbol', params);
4✔
430

431
        const result = await this.projectManager.getWorkspaceSymbol();
4✔
432
        return result;
4✔
433
    }
434

435
    @AddStackToErrorMessage
436
    public async onDocumentSymbol(params: DocumentSymbolParams) {
1✔
437
        this.logger.debug('onDocumentSymbol', params);
6✔
438

439
        const srcPath = util.uriToPath(params.textDocument.uri);
6✔
440
        const result = await this.projectManager.getDocumentSymbol({ srcPath: srcPath });
6✔
441
        return result;
6✔
442
    }
443

444
    @AddStackToErrorMessage
445
    public async onDefinition(params: TextDocumentPositionParams) {
1✔
446
        this.logger.debug('onDefinition', params);
5✔
447

448
        const srcPath = util.uriToPath(params.textDocument.uri);
5✔
449

450
        const result = this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
5✔
451
        return result;
5✔
452
    }
453

454
    @AddStackToErrorMessage
455
    public async onSignatureHelp(params: SignatureHelpParams) {
1✔
456
        this.logger.debug('onSignatureHelp', params);
4✔
457

458
        const srcPath = util.uriToPath(params.textDocument.uri);
4✔
459
        const result = await this.projectManager.getSignatureHelp({ srcPath: srcPath, position: params.position });
4✔
460
        if (result) {
4✔
461
            return result;
3✔
462
        } else {
463
            return {
1✔
464
                signatures: [],
465
                activeSignature: null,
466
                activeParameter: null
467
            };
468
        }
469

470
    }
471

472
    @AddStackToErrorMessage
473
    public async onReferences(params: ReferenceParams) {
1✔
474
        this.logger.debug('onReferences', params);
3✔
475

476
        const srcPath = util.uriToPath(params.textDocument.uri);
3✔
477
        const result = await this.projectManager.getReferences({ srcPath: srcPath, position: params.position });
3✔
478
        return result ?? [];
3!
479
    }
480

481

482
    @AddStackToErrorMessage
483
    private async onFullSemanticTokens(params: SemanticTokensParams) {
1✔
484
        this.logger.debug('onFullSemanticTokens', params);
1✔
485

486
        const srcPath = util.uriToPath(params.textDocument.uri);
1✔
487
        const result = await this.projectManager.getSemanticTokens({ srcPath: srcPath });
1✔
488

489
        return {
1✔
490
            data: encodeSemanticTokens(result)
491
        } as SemanticTokens;
492
    }
493

494
    @AddStackToErrorMessage
495
    public async onCodeAction(params: CodeActionParams) {
1✔
NEW
496
        this.logger.debug('onCodeAction', params);
×
497

NEW
498
        const srcPath = util.uriToPath(params.textDocument.uri);
×
NEW
499
        const result = await this.projectManager.getCodeActions({ srcPath: srcPath, range: params.range });
×
NEW
500
        return result;
×
501
    }
502

503

504
    @AddStackToErrorMessage
505
    public async onExecuteCommand(params: ExecuteCommandParams) {
1✔
506
        this.logger.debug('onExecuteCommand', params);
2✔
507

508
        if (params.command === CustomCommands.TranspileFile) {
2!
509
            const args = {
2✔
510
                srcPath: params.arguments[0] as string
511
            };
512
            const result = await this.projectManager.transpileFile(args);
2✔
513
            //back-compat: include `pathAbsolute` property so older vscode versions still work
514
            (result as any).pathAbsolute = result.srcPath;
2✔
515
            return result;
2✔
516
        }
517
    }
518

519
    /**
520
     * Establish a connection to the client if not already connected
521
     */
522
    private establishConnection() {
NEW
523
        if (!this.connection) {
×
NEW
524
            this.connection = createConnection(ProposedFeatures.all);
×
525
        }
NEW
526
        return this.connection;
×
527
    }
528

529
    /**
530
     * Send a new busy status notification to the client based on the current busy status
531
     */
532
    private sendBusyStatus() {
533
        this.busyStatusIndex = ++this.busyStatusIndex <= 0 ? 0 : this.busyStatusIndex;
222✔
534

535
        this.connection.sendNotification(NotificationName.busyStatus, {
222!
536
            status: this.projectManager.busyStatusTracker.status,
537
            timestamp: Date.now(),
538
            index: this.busyStatusIndex,
539
            activeRuns: [...this.projectManager.busyStatusTracker.activeRuns]
540
        })?.catch(logAndIgnoreError);
222!
541
    }
542
    private busyStatusIndex = -1;
41✔
543

544
    /**
545
     * Populate the path filterer with the client's include/exclude lists and the projects include lists
546
     * @returns the instance of the path filterer
547
     */
548
    private async rebuildPathFilterer() {
549
        this.pathFilterer.clear();
5✔
550
        const workspaceFolders = await this.connection.workspace.getWorkspaceFolders();
5✔
551
        await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
5✔
552
            const rootDir = util.uriToPath(workspaceFolder.uri);
5✔
553

554
            //always exclude everything from these common folders
555
            this.pathFilterer.registerExcludeList(rootDir, [
5✔
556
                '**/node_modules/**/*',
557
                '**/.git/**/*',
558
                'out/**/*',
559
                '**/.roku-deploy-staging/**/*'
560
            ]);
561
            //get any `files.exclude` patterns from the client from this workspace
562
            const workspaceExcludeGlobs = await this.getWorkspaceExcludeGlobs(rootDir);
5✔
563
            this.pathFilterer.registerExcludeList(rootDir, workspaceExcludeGlobs);
5✔
564

565
            //get any .gitignore patterns from the client from this workspace
566
            const gitignorePath = path.resolve(rootDir, '.gitignore');
5✔
567
            if (await fsExtra.pathExists(gitignorePath)) {
5!
NEW
568
                const matcher = ignore().add(
×
569
                    fsExtra.readFileSync(gitignorePath).toString()
570
                );
NEW
571
                this.pathFilterer.registerExcludeMatcher((path: string) => {
×
NEW
572
                    return matcher.test(path).ignored;
×
573
                });
574
            }
575
        }));
576
        this.logger.log('pathFilterer successfully reconstructed');
5✔
577

578
        return this.pathFilterer;
5✔
579
    }
580

581
    /**
582
     * Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
583
     */
584
    private async getWorkspaceExcludeGlobs(workspaceFolder: string): Promise<string[]> {
585
        const config = await this.getClientConfiguration<{ exclude: string[] }>(workspaceFolder, 'files');
37✔
586
        const result = Object
37✔
587
            .keys(config?.exclude ?? {})
222!
588
            .filter(x => config?.exclude?.[x])
3!
589
            //vscode files.exclude patterns support ignoring folders without needing to add `**/*`. So for our purposes, we need to
590
            //append **/* to everything without a file extension or magic at the end
591
            .map(pattern => [
3✔
592
                //send the pattern as-is (this handles weird cases and exact file matches)
593
                pattern,
594
                //treat the pattern as a directory (no harm in doing this because if it's a file, the pattern will just never match anything)
595
                `${pattern}/**/*`
596
            ])
597
            .flat(1);
598
        return result;
37✔
599
    }
600

601
    /**
602
     * Ask the project manager to sync all projects found within the list of workspaces
603
     * @param forceReload if true, all projects are discarded and recreated from scratch
604
     */
605
    private async syncProjects(forceReload = false) {
31✔
606
        // get all workspace paths from the client
607
        let workspaces = await Promise.all(
31✔
608
            (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => {
93!
609
                const workspaceFolder = util.uriToPath(x.uri);
32✔
610
                const config = await this.getClientConfiguration<BrightScriptClientConfiguration>(x.uri, 'brightscript');
32✔
611
                return {
32✔
612
                    workspaceFolder: workspaceFolder,
613
                    excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
614
                    bsconfigPath: config.configFile,
615
                    enableThreading: config.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault
192!
616

617
                } as WorkspaceConfig;
618
            })
619
        );
620

621
        await this.projectManager.syncProjects(workspaces, forceReload);
31✔
622

623
        //set our logLevel to the most verbose log level found across all projects and workspaces
624
        await this.syncLogLevel();
31✔
625
    }
626

627
    /**
628
     * Given a workspaceFolder path, get the specified configuration from the client (if applicable).
629
     * Be sure to use optional chaining to traverse the result in case that configuration doesn't exist or the client doesn't support `getConfiguration`
630
     * @param workspaceFolder the folder for the workspace in the client
631
     */
632
    private async getClientConfiguration<T extends Record<string, any>>(workspaceFolder: string, section: string): Promise<T> {
633
        let scopeUri: string;
634
        if (workspaceFolder.startsWith('file:')) {
146✔
635
            scopeUri = URI.parse(workspaceFolder).toString();
107✔
636
        } else {
637
            scopeUri = URI.file(workspaceFolder).toString();
39✔
638
        }
639
        let config = {};
146✔
640

641
        //if the client supports configuration, look for config group called "brightscript"
642
        if (this.hasConfigurationCapability) {
146✔
643
            config = await this.connection.workspace.getConfiguration({
139✔
644
                scopeUri: scopeUri,
645
                section: section
646
            });
647
        }
648
        return config as T;
146✔
649
    }
650

651
    /**
652
     * Send a critical failure notification to the client, which should show a notification of some kind
653
     */
654
    private sendCriticalFailure(message: string) {
NEW
655
        this.connection.sendNotification('critical-failure', message).catch(logAndIgnoreError);
×
656
    }
657

658
    /**
659
     * Send diagnostics to the client
660
     */
661
    private async sendDiagnostics(options: { project: LspProject; diagnostics: LspDiagnostic[] }) {
662
        const patch = this.diagnosticCollection.getPatch(options.project.projectNumber, options.diagnostics);
48✔
663

664
        await Promise.all(Object.keys(patch).map(async (srcPath) => {
48✔
665
            const uri = URI.file(srcPath).toString();
17✔
666
            const diagnostics = patch[srcPath].map(d => util.toDiagnostic(d, uri));
17✔
667

668
            await this.connection.sendDiagnostics({
17✔
669
                uri: uri,
670
                diagnostics: diagnostics
671
            });
672
        }));
673
    }
674
    private diagnosticCollection = new DiagnosticCollection();
41✔
675

676
    protected dispose() {
677
        this.loggerSubscription?.();
41✔
678
        this.projectManager?.dispose?.();
41!
679
    }
680
}
681

682
export enum CustomCommands {
1✔
683
    TranspileFile = 'TranspileFile'
1✔
684
}
685

686
export enum NotificationName {
1✔
687
    busyStatus = 'busyStatus'
1✔
688
}
689

690
/**
691
 * Wraps a method. If there's an error (either sync or via a promise),
692
 * this appends the error's stack trace at the end of the error message so that the connection will
693
 */
694
function AddStackToErrorMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
695
    let originalMethod = descriptor.value;
16✔
696

697
    //wrapping the original method
698
    descriptor.value = function value(...args: any[]) {
16✔
699
        try {
83✔
700
            let result = originalMethod.apply(this, args);
83✔
701
            //if the result looks like a promise, log if there's a rejection
702
            if (result?.then) {
83!
703
                return Promise.resolve(result).catch((e: Error) => {
82✔
704
                    if (e?.stack) {
×
705
                        e.message = e.stack;
×
706
                    }
707
                    return Promise.reject(e);
×
708
                });
709
            } else {
710
                return result;
1✔
711
            }
712
        } catch (e: any) {
713
            if (e?.stack) {
×
714
                e.message = e.stack;
×
715
            }
716
            throw e;
×
717
        }
718
    };
719
}
720

721
type Handler<T> = {
722
    [K in keyof T as K extends `on${string}` ? K : never]:
723
    T[K] extends (arg: infer U) => void ? (arg: U) => void : never;
724
};
725
// Extracts the argument type from the function and constructs the desired interface
726
export type OnHandler<T> = {
727
    [K in keyof Handler<T>]: Handler<T>[K] extends (arg: infer U) => void ? U : never;
728
};
729

730
interface BrightScriptClientConfiguration {
731
    configFile: string;
732
    languageServer: {
733
        enableThreading: boolean;
734
        logLevel: LogLevel | string;
735
    };
736
}
737

738
function logAndIgnoreError(error: Error) {
739
    if (error?.stack) {
2!
740
        error.message = error.stack;
2✔
741
    }
742
    console.error(error);
2✔
743
}
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