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

rokucommunity / brighterscript / #15046

03 Oct 2022 01:55PM UTC coverage: 87.532% (-0.3%) from 87.808%
#15046

push

TwitchBronBron
0.59.0

5452 of 6706 branches covered (81.3%)

Branch coverage included in aggregate %.

8259 of 8958 relevant lines covered (92.2%)

1521.92 hits per line

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

54.27
/src/LanguageServer.ts
1
import 'array-flat-polyfill';
1✔
2
import * as fastGlob from 'fast-glob';
1✔
3
import * as path from 'path';
1✔
4
import { rokuDeploy, util as rokuDeployUtil } from 'roku-deploy';
1✔
5
import type {
6
    CompletionItem,
7
    Connection,
8
    DidChangeWatchedFilesParams,
9
    InitializeParams,
10
    ServerCapabilities,
11
    TextDocumentPositionParams,
12
    ExecuteCommandParams,
13
    WorkspaceSymbolParams,
14
    SymbolInformation,
15
    DocumentSymbolParams,
16
    ReferenceParams,
17
    SignatureHelp,
18
    SignatureHelpParams,
19
    CodeActionParams,
20
    SemanticTokensOptions,
21
    SemanticTokens,
22
    SemanticTokensParams,
23
    TextDocumentChangeEvent,
24
    Hover
25
} from 'vscode-languageserver/node';
26
import {
1✔
27
    SemanticTokensRequest,
28
    createConnection,
29
    DidChangeConfigurationNotification,
30
    FileChangeType,
31
    ProposedFeatures,
32
    TextDocuments,
33
    TextDocumentSyncKind,
34
    CodeActionKind
35
} from 'vscode-languageserver/node';
36
import { URI } from 'vscode-uri';
1✔
37
import { TextDocument } from 'vscode-languageserver-textdocument';
1✔
38
import type { BsConfig } from './BsConfig';
39
import { Deferred } from './deferred';
1✔
40
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
41
import { ProgramBuilder } from './ProgramBuilder';
1✔
42
import { standardizePath as s, util } from './util';
1✔
43
import { Logger } from './Logger';
1✔
44
import { Throttler } from './Throttler';
1✔
45
import { KeyedThrottler } from './KeyedThrottler';
1✔
46
import { DiagnosticCollection } from './DiagnosticCollection';
1✔
47
import { isBrsFile } from './astUtils/reflection';
1✔
48
import { encodeSemanticTokens, semanticTokensLegend } from './SemanticTokenUtils';
1✔
49

50
export class LanguageServer {
1✔
51
    private connection = undefined as Connection;
34✔
52

53
    public projects = [] as Project[];
34✔
54

55
    /**
56
     * The number of milliseconds that should be used for language server typing debouncing
57
     */
58
    private debounceTimeout = 150;
34✔
59

60
    /**
61
     * These projects are created on the fly whenever a file is opened that is not included
62
     * in any of the workspace-based projects.
63
     * Basically these are single-file projects to at least get parsing for standalone files.
64
     * Also, they should only be created when the file is opened, and destroyed when the file is closed.
65
     */
66
    public standaloneFileProjects = {} as Record<string, Project>;
34✔
67

68
    private hasConfigurationCapability = false;
34✔
69

70
    /**
71
     * Indicates whether the client supports workspace folders
72
     */
73
    private clientHasWorkspaceFolderCapability = false;
34✔
74

75
    /**
76
     * Create a simple text document manager.
77
     * The text document manager supports full document sync only
78
     */
79
    private documents = new TextDocuments(TextDocument);
34✔
80

81
    private createConnection() {
82
        return createConnection(ProposedFeatures.all);
×
83
    }
84

85
    private loggerSubscription: () => void;
86

87
    private keyedThrottler = new KeyedThrottler(this.debounceTimeout);
34✔
88

89
    public validateThrottler = new Throttler(0);
34✔
90

91
    private sendDiagnosticsThrottler = new Throttler(0);
34✔
92

93
    private boundValidateAll = this.validateAll.bind(this);
34✔
94

95
    private validateAllThrottled() {
96
        return this.validateThrottler.run(this.boundValidateAll);
3✔
97
    }
98

99
    //run the server
100
    public run() {
101
        // Create a connection for the server. The connection uses Node's IPC as a transport.
102
        // Also include all preview / proposed LSP features.
103
        this.connection = this.createConnection();
13✔
104

105
        //listen to all of the output log events and pipe them into the debug channel in the extension
106
        this.loggerSubscription = Logger.subscribe((text) => {
13✔
107
            this.connection.tracer.log(text);
88✔
108
        });
109

110
        this.connection.onInitialize(this.onInitialize.bind(this));
13✔
111

112
        this.connection.onInitialized(this.onInitialized.bind(this)); //eslint-disable-line
13✔
113

114
        this.connection.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); //eslint-disable-line
13✔
115

116
        this.connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); //eslint-disable-line
13✔
117

118
        // The content of a text document has changed. This event is emitted
119
        // when the text document is first opened, when its content has changed,
120
        // or when document is closed without saving (original contents are sent as a change)
121
        //
122
        this.documents.onDidChangeContent(this.validateTextDocument.bind(this));
13✔
123

124
        //whenever a document gets closed
125
        this.documents.onDidClose(this.onDocumentClose.bind(this));
13✔
126

127
        // This handler provides the initial list of the completion items.
128
        this.connection.onCompletion(this.onCompletion.bind(this));
13✔
129

130
        // This handler resolves additional information for the item selected in
131
        // the completion list.
132
        this.connection.onCompletionResolve(this.onCompletionResolve.bind(this));
13✔
133

134
        this.connection.onHover(this.onHover.bind(this));
13✔
135

136
        this.connection.onExecuteCommand(this.onExecuteCommand.bind(this));
13✔
137

138
        this.connection.onDefinition(this.onDefinition.bind(this));
13✔
139

140
        this.connection.onDocumentSymbol(this.onDocumentSymbol.bind(this));
13✔
141

142
        this.connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this));
13✔
143

144
        this.connection.onSignatureHelp(this.onSignatureHelp.bind(this));
13✔
145

146
        this.connection.onReferences(this.onReferences.bind(this));
13✔
147

148
        this.connection.onCodeAction(this.onCodeAction.bind(this));
13✔
149

150
        //TODO switch to a more specific connection function call once they actually add it
151
        this.connection.onRequest(SemanticTokensRequest.method, this.onFullSemanticTokens.bind(this));
13✔
152

153
        /*
154
        this.connection.onDidOpenTextDocument((params) => {
155
             // A text document got opened in VSCode.
156
             // params.uri uniquely identifies the document. For documents stored on disk this is a file URI.
157
             // params.text the initial full content of the document.
158
            this.connection.console.log(`${params.textDocument.uri} opened.`);
159
        });
160
        this.connection.onDidChangeTextDocument((params) => {
161
             // The content of a text document did change in VSCode.
162
             // params.uri uniquely identifies the document.
163
             // params.contentChanges describe the content changes to the document.
164
            this.connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`);
165
        });
166
        this.connection.onDidCloseTextDocument((params) => {
167
             // A text document got closed in VSCode.
168
             // params.uri uniquely identifies the document.
169
            this.connection.console.log(`${params.textDocument.uri} closed.`);
170
        });
171
        */
172

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

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

180
    /**
181
     * Called when the client starts initialization
182
     * @param params
183
     */
184
    @AddStackToErrorMessage
185
    public onInitialize(params: InitializeParams) {
1✔
186
        let clientCapabilities = params.capabilities;
2✔
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);
2✔
191
        this.clientHasWorkspaceFolderCapability = !!(clientCapabilities.workspace && !!clientCapabilities.workspace.workspaceFolders);
2✔
192

193
        //return the capabilities of the server
194
        return {
2✔
195
            capabilities: {
196
                textDocumentSync: TextDocumentSyncKind.Full,
197
                // Tell the client that the server supports code completion
198
                completionProvider: {
199
                    resolveProvider: true,
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
    private initialProjectsCreated: Promise<any>;
229

230
    /**
231
     * Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
232
     */
233
    private async getWorkspaceExcludeGlobs(workspaceFolder: string): Promise<string[]> {
234
        let config = {
14✔
235
            exclude: {} as Record<string, boolean>
236
        };
237
        //if supported, ask vscode for the `files.exclude` configuration
238
        if (this.hasConfigurationCapability) {
14✔
239
            //get any `files.exclude` globs to use to filter
240
            config = await this.connection.workspace.getConfiguration({
12✔
241
                scopeUri: workspaceFolder,
242
                section: 'files'
243
            });
244
        }
245
        return Object
14✔
246
            .keys(config?.exclude ?? {})
84!
247
            .filter(x => config?.exclude?.[x])
1!
248
            //vscode files.exclude patterns support ignoring folders without needing to add `**/*`. So for our purposes, we need to
249
            //append **/* to everything without a file extension or magic at the end
250
            .map(pattern => [
1✔
251
                //send the pattern as-is (this handles weird cases and exact file matches)
252
                pattern,
253
                //treat the pattern as a directory (no harm in doing this because if it's a file, the pattern will just never match anything)
254
                `${pattern}/**/*`
255
            ])
256
            .flat(1)
257
            .concat([
258
                //always ignore projects from node_modules
259
                '**/node_modules/**/*'
260
            ]);
261
    }
262

263
    /**
264
     * Scan the workspace for all `bsconfig.json` files. If at least one is found, then only folders who have bsconfig.json are returned.
265
     * If none are found, then the workspaceFolder itself is treated as a project
266
     */
267
    private async getProjectPaths(workspaceFolder: string) {
268
        const excludes = (await this.getWorkspaceExcludeGlobs(workspaceFolder)).map(x => s`!${x}`);
15✔
269
        const files = await rokuDeploy.getFilePaths([
13✔
270
            '**/bsconfig.json',
271
            //exclude all files found in `files.exclude`
272
            ...excludes
273
        ], workspaceFolder);
274
        //if we found at least one bsconfig.json, then ALL projects must have a bsconfig.json.
275
        if (files.length > 0) {
13✔
276
            return files.map(file => s`${path.dirname(file.src)}`);
9✔
277
        }
278

279
        //look for roku project folders
280
        const rokuLikeDirs = (await Promise.all(
6✔
281
            //find all folders containing a `manifest` file
282
            (await rokuDeploy.getFilePaths([
283
                '**/manifest',
284
                ...excludes
285

286
                //is there at least one .bs|.brs file under the `/source` folder?
287
            ], workspaceFolder)).map(async manifestEntry => {
288
                const manifestDir = path.dirname(manifestEntry.src);
3✔
289
                const files = await rokuDeploy.getFilePaths([
3✔
290
                    'source/**/*.{brs,bs}',
291
                    ...excludes
292
                ], manifestDir);
293
                if (files.length > 0) {
3✔
294
                    return manifestDir;
2✔
295
                }
296
            })
297
            //throw out nulls
298
        )).filter(x => !!x);
3✔
299
        if (rokuLikeDirs.length > 0) {
6✔
300
            return rokuLikeDirs;
1✔
301
        }
302

303
        //treat the workspace folder as a brightscript project itself
304
        return [workspaceFolder];
5✔
305
    }
306

307
    /**
308
     * Find all folders with bsconfig.json files in them, and treat each as a project.
309
     * Treat workspaces that don't have a bsconfig.json as a project.
310
     * Handle situations where bsconfig.json files were added or removed (to elevate/lower workspaceFolder projects accordingly)
311
     * Leave existing projects alone if they are not affected by these changes
312
     */
313
    private async syncProjects() {
314
        const workspacePaths = await this.getWorkspacePaths();
12✔
315
        let projectPaths = (await Promise.all(
12✔
316
            workspacePaths.map(async workspacePath => {
317
                const projectPaths = await this.getProjectPaths(workspacePath);
13✔
318
                return projectPaths.map(projectPath => ({
16✔
319
                    projectPath: projectPath,
320
                    workspacePath: workspacePath
321
                }));
322
            })
323
        )).flat(1);
324

325
        //delete projects not represented in the list
326
        for (const project of this.getProjects()) {
12✔
327
            if (!projectPaths.find(x => x.projectPath === project.projectPath)) {
5✔
328
                this.removeProject(project);
3✔
329
            }
330
        }
331

332
        //exclude paths to projects we already have
333
        projectPaths = projectPaths.filter(x => {
12✔
334
            //only keep this project path if there's not a project with that path
335
            return !this.projects.find(project => project.projectPath === x.projectPath);
16✔
336
        });
337

338
        //dedupe by project path
339
        projectPaths = [
12✔
340
            ...projectPaths.reduce(
341
                (acc, x) => acc.set(x.projectPath, x),
15✔
342
                new Map<string, typeof projectPaths[0]>()
343
            ).values()
344
        ];
345

346
        //create missing projects
347
        await Promise.all(
12✔
348
            projectPaths.map(x => this.createProject(x.projectPath, x.workspacePath))
14✔
349
        );
350
        //flush diagnostics
351
        await this.sendDiagnostics();
12✔
352
    }
353

354
    /**
355
     * Get all workspace paths from the client
356
     */
357
    private async getWorkspacePaths() {
358
        let workspaceFolders = await this.connection.workspace.getWorkspaceFolders() ?? [];
12!
359
        return workspaceFolders.map((x) => {
12✔
360
            return util.uriToPath(x.uri);
13✔
361
        });
362
    }
363

364
    /**
365
     * Called when the client has finished initializing
366
     * @param params
367
     */
368
    @AddStackToErrorMessage
369
    private async onInitialized() {
1✔
370
        let projectCreatedDeferred = new Deferred();
1✔
371
        this.initialProjectsCreated = projectCreatedDeferred.promise;
1✔
372

373
        try {
1✔
374
            if (this.hasConfigurationCapability) {
1!
375
                // Register for all configuration changes.
376
                await this.connection.client.register(
×
377
                    DidChangeConfigurationNotification.type,
378
                    undefined
379
                );
380
            }
381

382
            await this.syncProjects();
1✔
383

384
            if (this.clientHasWorkspaceFolderCapability) {
1!
385
                this.connection.workspace.onDidChangeWorkspaceFolders(async (evt) => {
×
386
                    await this.syncProjects();
×
387
                });
388
            }
389
            await this.waitAllProjectFirstRuns(false);
1✔
390
            projectCreatedDeferred.resolve();
1✔
391
        } catch (e: any) {
392
            this.sendCriticalFailure(
×
393
                `Critical failure during BrighterScript language server startup.
394
                Please file a github issue and include the contents of the 'BrighterScript Language Server' output channel.
395

396
                Error message: ${e.message}`
397
            );
398
            throw e;
×
399
        }
400
    }
401

402
    /**
403
     * Send a critical failure notification to the client, which should show a notification of some kind
404
     */
405
    private sendCriticalFailure(message: string) {
406
        this.connection.sendNotification('critical-failure', message);
×
407
    }
408

409
    /**
410
     * Wait for all programs' first run to complete
411
     */
412
    private async waitAllProjectFirstRuns(waitForFirstProject = true) {
28✔
413
        if (waitForFirstProject) {
29✔
414
            await this.initialProjectsCreated;
28✔
415
        }
416

417
        let status: string;
418
        for (let project of this.getProjects()) {
29✔
419
            try {
29✔
420
                await project.firstRunPromise;
29✔
421
            } catch (e: any) {
422
                status = 'critical-error';
×
423
                //the first run failed...that won't change unless we reload the workspace, so replace with resolved promise
424
                //so we don't show this error again
425
                project.firstRunPromise = Promise.resolve();
×
426
                this.sendCriticalFailure(`BrighterScript language server failed to start: \n${e.message}`);
×
427
            }
428
        }
429
        this.connection.sendNotification('build-status', status ? status : 'success');
29!
430
    }
431

432
    /**
433
     * Event handler for when the program wants to load file contents.
434
     * anytime the program wants to load a file, check with our in-memory document cache first
435
     */
436
    private documentFileResolver(srcPath: string) {
437
        let pathUri = URI.file(srcPath).toString();
12✔
438
        let document = this.documents.get(pathUri);
12✔
439
        if (document) {
12!
440
            return document.getText();
×
441
        }
442
    }
443

444
    private async getConfigFilePath(workspacePath: string) {
445
        let scopeUri: string;
446
        if (workspacePath.startsWith('file:')) {
33!
447
            scopeUri = URI.parse(workspacePath).toString();
×
448
        } else {
449
            scopeUri = URI.file(workspacePath).toString();
33✔
450
        }
451
        let config = {
33✔
452
            configFile: undefined
453
        };
454
        //if the client supports configuration, look for config group called "brightscript"
455
        if (this.hasConfigurationCapability) {
33✔
456
            config = await this.connection.workspace.getConfiguration({
31✔
457
                scopeUri: scopeUri,
458
                section: 'brightscript'
459
            });
460
        }
461
        let configFilePath: string;
462

463
        //if there's a setting, we need to find the file or show error if it can't be found
464
        if (config?.configFile) {
33!
465
            configFilePath = path.resolve(workspacePath, config.configFile);
1✔
466
            if (await util.pathExists(configFilePath)) {
1!
467
                return configFilePath;
1✔
468
            } else {
469
                this.sendCriticalFailure(`Cannot find config file specified in user / workspace settings at '${configFilePath}'`);
×
470
            }
471
        }
472

473
        //default to config file path found in the root of the workspace
474
        configFilePath = path.resolve(workspacePath, 'bsconfig.json');
32✔
475
        if (await util.pathExists(configFilePath)) {
32✔
476
            return configFilePath;
8✔
477
        }
478

479
        //look for the deprecated `brsconfig.json` file
480
        configFilePath = path.resolve(workspacePath, 'brsconfig.json');
24✔
481
        if (await util.pathExists(configFilePath)) {
24!
482
            return configFilePath;
×
483
        }
484

485
        //no config file could be found
486
        return undefined;
24✔
487
    }
488

489
    private async createProject(projectPath: string, workspacePath = projectPath) {
17✔
490
        workspacePath ??= projectPath;
31!
491
        let project = this.projects.find((x) => x.projectPath === projectPath);
31✔
492
        //skip this project if we already have it
493
        if (project) {
31!
494
            return;
×
495
        }
496

497
        let builder = new ProgramBuilder();
31✔
498

499
        //flush diagnostics every time the program finishes validating
500
        builder.plugins.add({
31✔
501
            name: 'bsc-language-server',
502
            afterProgramValidate: () => {
503
                void this.sendDiagnostics();
36✔
504
            }
505
        });
506

507
        //prevent clearing the console on run...this isn't the CLI so we want to keep a full log of everything
508
        builder.allowConsoleClearing = false;
31✔
509

510
        //look for files in our in-memory cache before going to the file system
511
        builder.addFileResolver(this.documentFileResolver.bind(this));
31✔
512

513
        let configFilePath = await this.getConfigFilePath(projectPath);
31✔
514

515
        let cwd = projectPath;
31✔
516

517
        //if the config file exists, use it and its folder as cwd
518
        if (configFilePath && await util.pathExists(configFilePath)) {
31✔
519
            cwd = path.dirname(configFilePath);
7✔
520
        } else {
521
            //config file doesn't exist...let `brighterscript` resolve the default way
522
            configFilePath = undefined;
24✔
523
        }
524

525
        let firstRunPromise = builder.run({
31✔
526
            cwd: cwd,
527
            project: configFilePath,
528
            watch: false,
529
            createPackage: false,
530
            deploy: false,
531
            copyToStaging: false,
532
            showDiagnosticsInConsole: false
533
        });
534
        firstRunPromise.catch((err) => {
31✔
535
            console.error(err);
×
536
        });
537

538
        let newProject: Project = {
31✔
539
            builder: builder,
540
            firstRunPromise: firstRunPromise,
541
            projectPath: projectPath,
542
            workspacePath: workspacePath,
543
            isFirstRunComplete: false,
544
            isFirstRunSuccessful: false,
545
            configFilePath: configFilePath,
546
            isStandaloneFileProject: false
547
        };
548

549
        this.projects.push(newProject);
31✔
550

551
        await firstRunPromise.then(() => {
31✔
552
            newProject.isFirstRunComplete = true;
31✔
553
            newProject.isFirstRunSuccessful = true;
31✔
554
        }).catch(() => {
555
            newProject.isFirstRunComplete = true;
×
556
            newProject.isFirstRunSuccessful = false;
×
557
        }).then(() => {
558
            //if we found a deprecated brsconfig.json, add a diagnostic warning the user
559
            if (configFilePath && path.basename(configFilePath) === 'brsconfig.json') {
31!
560
                builder.addDiagnostic(configFilePath, {
×
561
                    ...DiagnosticMessages.brsConfigJsonIsDeprecated(),
562
                    range: util.createRange(0, 0, 0, 0)
563
                });
564
                return this.sendDiagnostics();
×
565
            }
566
        });
567
    }
568

569
    private async createStandaloneFileProject(srcPath: string) {
570
        //skip this workspace if we already have it
571
        if (this.standaloneFileProjects[srcPath]) {
3✔
572
            return this.standaloneFileProjects[srcPath];
1✔
573
        }
574

575
        let builder = new ProgramBuilder();
2✔
576

577
        //prevent clearing the console on run...this isn't the CLI so we want to keep a full log of everything
578
        builder.allowConsoleClearing = false;
2✔
579

580
        //look for files in our in-memory cache before going to the file system
581
        builder.addFileResolver(this.documentFileResolver.bind(this));
2✔
582

583
        //get the path to the directory where this file resides
584
        let cwd = path.dirname(srcPath);
2✔
585

586
        //get the closest config file and use most of the settings from that
587
        let configFilePath = await util.findClosestConfigFile(srcPath);
2✔
588
        let project: BsConfig = {};
2✔
589
        if (configFilePath) {
2!
590
            project = util.normalizeAndResolveConfig({ project: configFilePath });
×
591
        }
592
        //override the rootDir and files array
593
        project.rootDir = cwd;
2✔
594
        project.files = [{
2✔
595
            src: srcPath,
596
            dest: path.basename(srcPath)
597
        }];
598

599
        let firstRunPromise = builder.run({
2✔
600
            ...project,
601
            cwd: cwd,
602
            project: configFilePath,
603
            watch: false,
604
            createPackage: false,
605
            deploy: false,
606
            copyToStaging: false,
607
            diagnosticFilters: [
608
                //hide the "file not referenced by any other file" error..that's expected in a standalone file.
609
                1013
610
            ]
611
        }).catch((err) => {
612
            console.error(err);
×
613
        });
614

615
        let newProject: Project = {
2✔
616
            builder: builder,
617
            firstRunPromise: firstRunPromise,
618
            projectPath: srcPath,
619
            workspacePath: srcPath,
620
            isFirstRunComplete: false,
621
            isFirstRunSuccessful: false,
622
            configFilePath: configFilePath,
623
            isStandaloneFileProject: true
624
        };
625

626
        this.standaloneFileProjects[srcPath] = newProject;
2✔
627

628
        await firstRunPromise.then(() => {
2✔
629
            newProject.isFirstRunComplete = true;
2✔
630
            newProject.isFirstRunSuccessful = true;
2✔
631
        }).catch(() => {
632
            newProject.isFirstRunComplete = true;
×
633
            newProject.isFirstRunSuccessful = false;
×
634
        });
635
        return newProject;
2✔
636
    }
637

638
    private getProjects() {
639
        let projects = this.projects.slice();
70✔
640
        for (let key in this.standaloneFileProjects) {
70✔
641
            projects.push(this.standaloneFileProjects[key]);
×
642
        }
643
        return projects;
70✔
644
    }
645

646
    /**
647
     * Provide a list of completion items based on the current cursor position
648
     * @param textDocumentPosition
649
     */
650
    @AddStackToErrorMessage
651
    private async onCompletion(params: TextDocumentPositionParams) {
1✔
652
        //ensure programs are initialized
653
        await this.waitAllProjectFirstRuns();
×
654

655
        let filePath = util.uriToPath(params.textDocument.uri);
×
656

657
        //wait until the file has settled
658
        await this.keyedThrottler.onIdleOnce(filePath, true);
×
659

660
        let completions = this
×
661
            .getProjects()
662
            .flatMap(workspace => workspace.builder.program.getCompletions(filePath, params.position));
×
663

664
        for (let completion of completions) {
×
665
            completion.commitCharacters = ['.'];
×
666
        }
667

668
        return completions;
×
669
    }
670

671
    /**
672
     * Provide a full completion item from the selection
673
     * @param item
674
     */
675
    @AddStackToErrorMessage
676
    private onCompletionResolve(item: CompletionItem): CompletionItem {
1✔
677
        if (item.data === 1) {
×
678
            item.detail = 'TypeScript details';
×
679
            item.documentation = 'TypeScript documentation';
×
680
        } else if (item.data === 2) {
×
681
            item.detail = 'JavaScript details';
×
682
            item.documentation = 'JavaScript documentation';
×
683
        }
684
        return item;
×
685
    }
686

687
    @AddStackToErrorMessage
688
    private async onCodeAction(params: CodeActionParams) {
1✔
689
        //ensure programs are initialized
690
        await this.waitAllProjectFirstRuns();
×
691

692
        let srcPath = util.uriToPath(params.textDocument.uri);
×
693

694
        //wait until the file has settled
695
        await this.keyedThrottler.onIdleOnce(srcPath, true);
×
696

697
        const codeActions = this
×
698
            .getProjects()
699
            //skip programs that don't have this file
700
            .filter(x => x.builder?.program?.hasFile(srcPath))
×
701
            .flatMap(workspace => workspace.builder.program.getCodeActions(srcPath, params.range));
×
702

703
        //clone the diagnostics for each code action, since certain diagnostics can have circular reference properties that kill the language server if serialized
704
        for (const codeAction of codeActions) {
×
705
            if (codeAction.diagnostics) {
×
706
                codeAction.diagnostics = codeAction.diagnostics.map(x => util.toDiagnostic(x, params.textDocument.uri));
×
707
            }
708
        }
709
        return codeActions;
×
710
    }
711

712
    /**
713
     * Remove a project from the language server
714
     */
715
    private removeProject(project: Project) {
716
        const idx = this.projects.indexOf(project);
3✔
717
        if (idx > -1) {
3!
718
            this.projects.splice(idx, 1);
3✔
719
        }
720
        project?.builder?.dispose();
3!
721
    }
722

723
    /**
724
     * Reload each of the specified workspaces
725
     */
726
    private async reloadProjects(projects: Project[]) {
727
        await Promise.all(
×
728
            projects.map(async (project) => {
729
                //ensure the workspace has finished starting up
730
                try {
×
731
                    await project.firstRunPromise;
×
732
                } catch (e) { }
733

734
                //handle standard workspace
735
                if (project.isStandaloneFileProject === false) {
×
736
                    this.removeProject(project);
×
737

738
                    //create a new workspace/brs program
739
                    await this.createProject(project.projectPath, project.workspacePath);
×
740

741
                    //handle temp workspace
742
                } else {
743
                    project.builder.dispose();
×
744
                    delete this.standaloneFileProjects[project.projectPath];
×
745
                    await this.createStandaloneFileProject(project.projectPath);
×
746
                }
747
            })
748
        );
749
        if (projects.length > 0) {
×
750
            //wait for all of the programs to finish starting up
751
            await this.waitAllProjectFirstRuns();
×
752

753
            // valdiate all workspaces
754
            this.validateAllThrottled(); //eslint-disable-line
×
755
        }
756
    }
757

758
    private getRootDir(workspace: Project) {
759
        let options = workspace?.builder?.program?.options;
×
760
        return options?.rootDir ?? options?.cwd;
×
761
    }
762

763
    /**
764
     * Sometimes users will alter their bsconfig files array, and will include standalone files.
765
     * If this is the case, those standalone workspaces should be removed because the file was
766
     * included in an actual program now.
767
     *
768
     * Sometimes files that used to be included are now excluded, so those open files need to be re-processed as standalone
769
     */
770
    private async synchronizeStandaloneProjects() {
771

772
        //remove standalone workspaces that are now included in projects
773
        for (let standaloneFilePath in this.standaloneFileProjects) {
3✔
774
            let standaloneProject = this.standaloneFileProjects[standaloneFilePath];
×
775
            for (let project of this.projects) {
×
776
                await standaloneProject.firstRunPromise;
×
777

778
                let dest = rokuDeploy.getDestPath(
×
779
                    standaloneFilePath,
780
                    project?.builder?.program?.options?.files ?? [],
×
781
                    this.getRootDir(project)
782
                );
783
                //destroy this standalone workspace because the file has now been included in an actual workspace,
784
                //or if the workspace wants the file
785
                if (project?.builder?.program?.hasFile(standaloneFilePath) || dest) {
×
786
                    standaloneProject.builder.dispose();
×
787
                    delete this.standaloneFileProjects[standaloneFilePath];
×
788
                }
789
            }
790
        }
791

792
        //create standalone projects for open files that no longer have a project
793
        let textDocuments = this.documents.all();
3✔
794
        outer: for (let textDocument of textDocuments) {
3✔
795
            let filePath = URI.parse(textDocument.uri).fsPath;
×
796
            for (let project of this.getProjects()) {
×
797
                let dest = rokuDeploy.getDestPath(
×
798
                    filePath,
799
                    project?.builder?.program?.options?.files ?? [],
×
800
                    this.getRootDir(project)
801
                );
802
                //if this project has the file, or it wants the file, do NOT make a standaloneProject for this file
803
                if (project?.builder?.program?.hasFile(filePath) || dest) {
×
804
                    continue outer;
×
805
                }
806
            }
807
            //if we got here, no workspace has this file, so make a standalone file workspace
808
            let project = await this.createStandaloneFileProject(filePath);
×
809
            await project.firstRunPromise;
×
810
        }
811
    }
812

813
    @AddStackToErrorMessage
814
    private async onDidChangeConfiguration() {
1✔
815
        if (this.hasConfigurationCapability) {
×
816
            //if the user changes any config value, just mass-reload all projects
817
            await this.reloadProjects(this.getProjects());
×
818
            // Reset all cached document settings
819
        } else {
820
            // this.globalSettings = <ExampleSettings>(
821
            //     (change.settings.languageServerExample || this.defaultSettings)
822
            // );
823
        }
824
    }
825

826
    /**
827
     * Called when watched files changed (add/change/delete).
828
     * The CLIENT is in charge of what files to watch, so all client
829
     * implementations should ensure that all valid project
830
     * file types are watched (.brs,.bs,.xml,manifest, and any json/text/image files)
831
     * @param params
832
     */
833
    @AddStackToErrorMessage
834
    private async onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) {
1✔
835
        //ensure programs are initialized
836
        await this.waitAllProjectFirstRuns();
3✔
837

838
        this.connection.sendNotification('build-status', 'building');
3✔
839

840
        let projects = this.getProjects();
3✔
841

842
        //convert all file paths to absolute paths
843
        let changes = params.changes.map(x => {
3✔
844
            return {
4✔
845
                type: x.type,
846
                srcPath: s`${URI.parse(x.uri).fsPath}`
847
            };
848
        });
849

850
        let keys = changes.map(x => x.srcPath);
4✔
851

852
        //filter the list of changes to only the ones that made it through the debounce unscathed
853
        changes = changes.filter(x => keys.includes(x.srcPath));
4✔
854

855
        //if we have changes to work with
856
        if (changes.length > 0) {
3!
857

858
            //if any bsconfig files were added or deleted, re-sync all projects instead of the more specific approach below
859
            if (changes.find(x => (x.type === FileChangeType.Created || x.type === FileChangeType.Deleted) && path.basename(x.srcPath).toLowerCase() === 'bsconfig.json')) {
4!
860
                return this.syncProjects();
×
861
            }
862

863
            //reload any workspace whose bsconfig.json file has changed
864
            {
865
                let projectsToReload = [] as Project[];
3✔
866
                //get the file paths as a string array
867
                let filePaths = changes.map((x) => x.srcPath);
4✔
868

869
                for (let project of projects) {
3✔
870
                    if (project.configFilePath && filePaths.includes(project.configFilePath)) {
3!
871
                        projectsToReload.push(project);
×
872
                    }
873
                }
874
                if (projectsToReload.length > 0) {
3!
875
                    //vsc can generate a ton of these changes, for vsc system files, so we need to bail if there's no work to do on any of our actual project files
876
                    //reload any projects that need to be reloaded
877
                    await this.reloadProjects(projectsToReload);
×
878
                }
879

880
                //reassign `projects` to the non-reloaded projects
881
                projects = projects.filter(x => !projectsToReload.includes(x));
3✔
882
            }
883

884
            //convert created folders into a list of files of their contents
885
            const directoryChanges = changes
3✔
886
                //get only creation items
887
                .filter(change => change.type === FileChangeType.Created)
4✔
888
                //keep only the directories
889
                .filter(change => util.isDirectorySync(change.srcPath));
3✔
890

891
            //remove the created directories from the changes array (we will add back each of their files next)
892
            changes = changes.filter(x => !directoryChanges.includes(x));
4✔
893

894
            //look up every file in each of the newly added directories
895
            const newFileChanges = directoryChanges
3✔
896
                //take just the path
897
                .map(x => x.srcPath)
2✔
898
                //exclude the roku deploy staging folder
899
                .filter(dirPath => !dirPath.includes('.roku-deploy-staging'))
2✔
900
                //get the files for each folder recursively
901
                .flatMap(dirPath => {
902
                    //look up all files
903
                    let files = fastGlob.sync('**/*', {
2✔
904
                        absolute: true,
905
                        cwd: rokuDeployUtil.toForwardSlashes(dirPath)
906
                    });
907
                    return files.map(x => {
2✔
908
                        return {
5✔
909
                            type: FileChangeType.Created,
910
                            srcPath: s`${x}`
911
                        };
912
                    });
913
                });
914

915
            //add the new file changes to the changes array.
916
            changes.push(...newFileChanges as any);
3✔
917

918
            //give every workspace the chance to handle file changes
919
            await Promise.all(
3✔
920
                projects.map((project) => this.handleFileChanges(project, changes))
3✔
921
            );
922
        }
923
        this.connection.sendNotification('build-status', 'success');
3✔
924
    }
925

926
    /**
927
     * This only operates on files that match the specified files globs, so it is safe to throw
928
     * any file changes you receive with no unexpected side-effects
929
     * @param changes
930
     */
931
    public async handleFileChanges(project: Project, changes: { type: FileChangeType; srcPath: string }[]) {
932
        //this loop assumes paths are both file paths and folder paths, which eliminates the need to detect.
933
        //All functions below can handle being given a file path AND a folder path, and will only operate on the one they are looking for
934
        let consumeCount = 0;
5✔
935
        await Promise.all(changes.map(async (change) => {
5✔
936
            await this.keyedThrottler.run(change.srcPath, async () => {
9✔
937
                consumeCount += await this.handleFileChange(project, change) ? 1 : 0;
9✔
938
            });
939
        }));
940

941
        if (consumeCount > 0) {
5✔
942
            await this.validateAllThrottled();
3✔
943
        }
944
    }
945

946
    /**
947
     * This only operates on files that match the specified files globs, so it is safe to throw
948
     * any file changes you receive with no unexpected side-effects
949
     * @param changes
950
     */
951
    private async handleFileChange(project: Project, change: { type: FileChangeType; srcPath: string }) {
952
        const { program, options, rootDir } = project.builder;
9✔
953

954
        //deleted
955
        if (change.type === FileChangeType.Deleted) {
9!
956
            //try to act on this path as a directory
957
            project.builder.removeFilesInFolder(change.srcPath);
×
958

959
            //if this is a file loaded in the program, remove it
960
            if (program.hasFile(change.srcPath)) {
×
961
                program.removeFile(change.srcPath);
×
962
                return true;
×
963
            } else {
964
                return false;
×
965
            }
966

967
            //created
968
        } else if (change.type === FileChangeType.Created) {
9✔
969
            // thanks to `onDidChangeWatchedFiles`, we can safely assume that all "Created" changes are file paths, (not directories)
970

971
            //get the dest path for this file.
972
            let destPath = rokuDeploy.getDestPath(change.srcPath, options.files, rootDir);
8✔
973

974
            //if we got a dest path, then the program wants this file
975
            if (destPath) {
8✔
976
                program.setFile(
4✔
977
                    {
978
                        src: change.srcPath,
979
                        dest: rokuDeploy.getDestPath(change.srcPath, options.files, rootDir)
980
                    },
981
                    await project.builder.getFileContents(change.srcPath)
982
                );
983
                return true;
4✔
984
            } else {
985
                //no dest path means the program doesn't want this file
986
                return false;
4✔
987
            }
988

989
            //changed
990
        } else if (program.hasFile(change.srcPath)) {
1!
991
            //sometimes "changed" events are emitted on files that were actually deleted,
992
            //so determine file existance and act accordingly
993
            if (await util.pathExists(change.srcPath)) {
×
994
                program.setFile(
×
995
                    {
996
                        src: change.srcPath,
997
                        dest: rokuDeploy.getDestPath(change.srcPath, options.files, rootDir)
998
                    },
999
                    await project.builder.getFileContents(change.srcPath)
1000
                );
1001
            } else {
1002
                program.removeFile(change.srcPath);
×
1003
            }
1004
            return true;
×
1005
        }
1006
    }
1007

1008
    @AddStackToErrorMessage
1009
    private async onHover(params: TextDocumentPositionParams) {
1✔
1010
        //ensure programs are initialized
1011
        await this.waitAllProjectFirstRuns();
×
1012

1013
        const srcPath = util.uriToPath(params.textDocument.uri);
×
1014
        let projects = this.getProjects();
×
1015
        let hovers = projects
×
1016
            //get hovers from all projects
1017
            .map((x) => x.builder.program.getHover(srcPath, params.position))
×
1018
            //flatten to a single list
1019
            .flat();
1020

1021
        const contents = [
×
1022
            ...(hovers ?? [])
×
1023
                //pull all hover contents out into a flag array of strings
1024
                .map(x => {
1025
                    return Array.isArray(x?.contents) ? x?.contents : [x?.contents];
×
1026
                }).flat()
1027
                //remove nulls
1028
                .filter(x => !!x)
×
1029
                //dedupe hovers across all projects
1030
                .reduce((set, content) => set.add(content), new Set<string>()).values()
×
1031
        ];
1032

1033
        if (contents.length > 0) {
×
1034
            let hover: Hover = {
×
1035
                //use the range from the first hover
1036
                range: hovers[0]?.range,
×
1037
                //the contents of all hovers
1038
                contents: contents
1039
            };
1040
            return hover;
×
1041
        }
1042
    }
1043

1044
    @AddStackToErrorMessage
1045
    private async onDocumentClose(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
1✔
1046
        const { document } = event;
×
1047
        let filePath = URI.parse(document.uri).fsPath;
×
1048
        let standaloneFileProject = this.standaloneFileProjects[filePath];
×
1049
        //if this was a temp file, close it
1050
        if (standaloneFileProject) {
×
1051
            await standaloneFileProject.firstRunPromise;
×
1052
            standaloneFileProject.builder.dispose();
×
1053
            delete this.standaloneFileProjects[filePath];
×
1054
            await this.sendDiagnostics();
×
1055
        }
1056
    }
1057

1058
    @AddStackToErrorMessage
1059
    private async validateTextDocument(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
1✔
1060
        const { document } = event;
×
1061
        //ensure programs are initialized
1062
        await this.waitAllProjectFirstRuns();
×
1063

1064
        let filePath = URI.parse(document.uri).fsPath;
×
1065

1066
        try {
×
1067

1068
            //throttle file processing. first call is run immediately, and then the last call is processed.
1069
            await this.keyedThrottler.run(filePath, () => {
×
1070

1071
                this.connection.sendNotification('build-status', 'building');
×
1072

1073
                let documentText = document.getText();
×
1074
                for (const project of this.getProjects()) {
×
1075
                    //only add or replace existing files. All of the files in the project should
1076
                    //have already been loaded by other means
1077
                    if (project.builder.program.hasFile(filePath)) {
×
1078
                        let rootDir = project.builder.program.options.rootDir ?? project.builder.program.options.cwd;
×
1079
                        let dest = rokuDeploy.getDestPath(filePath, project.builder.program.options.files, rootDir);
×
1080
                        project.builder.program.setFile({
×
1081
                            src: filePath,
1082
                            dest: dest
1083
                        }, documentText);
1084
                    }
1085
                }
1086
            });
1087
            // validate all projects
1088
            await this.validateAllThrottled();
×
1089
        } catch (e: any) {
1090
            this.sendCriticalFailure(`Critical error parsing / validating ${filePath}: ${e.message}`);
×
1091
        }
1092
    }
1093

1094
    private async validateAll() {
1095
        try {
3✔
1096
            //synchronize parsing for open files that were included/excluded from projects
1097
            await this.synchronizeStandaloneProjects();
3✔
1098

1099
            let projects = this.getProjects();
3✔
1100

1101
            //validate all programs
1102
            await Promise.all(
3✔
1103
                projects.map((x) => x.builder.program.validate())
2✔
1104
            );
1105
        } catch (e: any) {
1106
            this.connection.console.error(e);
×
1107
            this.sendCriticalFailure(`Critical error validating project: ${e.message}${e.stack ?? ''}`);
×
1108
        }
1109

1110
        this.connection.sendNotification('build-status', 'success');
3✔
1111
    }
1112

1113
    @AddStackToErrorMessage
1114
    public async onWorkspaceSymbol(params: WorkspaceSymbolParams) {
1✔
1115
        await this.waitAllProjectFirstRuns();
4✔
1116

1117
        const results = util.flatMap(
4✔
1118
            await Promise.all(this.getProjects().map(project => {
1119
                return project.builder.program.getWorkspaceSymbols();
4✔
1120
            })),
1121
            c => c
4✔
1122
        );
1123

1124
        // Remove duplicates
1125
        const allSymbols = Object.values(results.reduce((map, symbol) => {
4✔
1126
            const key = symbol.location.uri + symbol.name;
24✔
1127
            map[key] = symbol;
24✔
1128
            return map;
24✔
1129
        }, {}));
1130
        return allSymbols as SymbolInformation[];
4✔
1131
    }
1132

1133
    @AddStackToErrorMessage
1134
    public async onDocumentSymbol(params: DocumentSymbolParams) {
1✔
1135
        await this.waitAllProjectFirstRuns();
6✔
1136

1137
        await this.keyedThrottler.onIdleOnce(util.uriToPath(params.textDocument.uri), true);
6✔
1138

1139
        const srcPath = util.uriToPath(params.textDocument.uri);
6✔
1140
        for (const project of this.getProjects()) {
6✔
1141
            const file = project.builder.program.getFile(srcPath);
6✔
1142
            if (isBrsFile(file)) {
6!
1143
                return file.getDocumentSymbols();
6✔
1144
            }
1145
        }
1146
    }
1147

1148
    @AddStackToErrorMessage
1149
    private async onDefinition(params: TextDocumentPositionParams) {
1✔
1150
        await this.waitAllProjectFirstRuns();
5✔
1151

1152
        const srcPath = util.uriToPath(params.textDocument.uri);
5✔
1153

1154
        const results = util.flatMap(
5✔
1155
            await Promise.all(this.getProjects().map(project => {
1156
                return project.builder.program.getDefinition(srcPath, params.position);
5✔
1157
            })),
1158
            c => c
5✔
1159
        );
1160
        return results;
5✔
1161
    }
1162

1163
    @AddStackToErrorMessage
1164
    private async onSignatureHelp(params: SignatureHelpParams) {
1✔
1165
        await this.waitAllProjectFirstRuns();
3✔
1166

1167
        const filepath = util.uriToPath(params.textDocument.uri);
3✔
1168
        await this.keyedThrottler.onIdleOnce(filepath, true);
3✔
1169

1170
        try {
3✔
1171
            const signatures = util.flatMap(
3✔
1172
                await Promise.all(this.getProjects().map(project => project.builder.program.getSignatureHelp(filepath, params.position)
3✔
1173
                )),
1174
                c => c
3✔
1175
            );
1176

1177
            const activeSignature = signatures.length > 0 ? 0 : null;
3!
1178

1179
            const activeParameter = activeSignature >= 0 ? signatures[activeSignature]?.index : null;
3!
1180

1181
            let results: SignatureHelp = {
3✔
1182
                signatures: signatures.map((s) => s.signature),
3✔
1183
                activeSignature: activeSignature,
1184
                activeParameter: activeParameter
1185
            };
1186
            return results;
3✔
1187
        } catch (e: any) {
1188
            this.connection.console.error(`error in onSignatureHelp: ${e.stack ?? e.message ?? e}`);
×
1189
            return {
×
1190
                signatures: [],
1191
                activeSignature: 0,
1192
                activeParameter: 0
1193
            };
1194
        }
1195
    }
1196

1197
    @AddStackToErrorMessage
1198
    private async onReferences(params: ReferenceParams) {
1✔
1199
        await this.waitAllProjectFirstRuns();
3✔
1200

1201
        const position = params.position;
3✔
1202
        const srcPath = util.uriToPath(params.textDocument.uri);
3✔
1203

1204
        const results = util.flatMap(
3✔
1205
            await Promise.all(this.getProjects().map(project => {
1206
                return project.builder.program.getReferences(srcPath, position);
3✔
1207
            })),
1208
            c => c
3✔
1209
        );
1210
        return results.filter((r) => r);
5✔
1211
    }
1212

1213
    @AddStackToErrorMessage
1214
    private async onFullSemanticTokens(params: SemanticTokensParams) {
1✔
1215
        await this.waitAllProjectFirstRuns();
×
1216
        await this.keyedThrottler.onIdleOnce(util.uriToPath(params.textDocument.uri), true);
×
1217

1218
        const srcPath = util.uriToPath(params.textDocument.uri);
×
1219
        for (const project of this.projects) {
×
1220
            //find the first program that has this file, since it would be incredibly inefficient to generate semantic tokens for the same file multiple times.
1221
            if (project.builder.program.hasFile(srcPath)) {
×
1222
                let semanticTokens = project.builder.program.getSemanticTokens(srcPath);
×
1223
                return {
×
1224
                    data: encodeSemanticTokens(semanticTokens)
1225
                } as SemanticTokens;
1226
            }
1227
        }
1228
    }
1229

1230
    private diagnosticCollection = new DiagnosticCollection();
34✔
1231

1232
    private async sendDiagnostics() {
1233
        await this.sendDiagnosticsThrottler.run(async () => {
50✔
1234
            //wait for all programs to finish running. This ensures the `Program` exists.
1235
            await Promise.all(
50✔
1236
                this.projects.map(x => x.firstRunPromise)
60✔
1237
            );
1238

1239
            //Get only the changes to diagnostics since the last time we sent them to the client
1240
            const patch = this.diagnosticCollection.getPatch(this.projects);
50✔
1241

1242
            for (let filePath in patch) {
50✔
1243
                const uri = URI.file(filePath).toString();
5✔
1244
                const diagnostics = patch[filePath].map(d => util.toDiagnostic(d, uri));
5✔
1245

1246
                this.connection.sendDiagnostics({
5✔
1247
                    uri: uri,
1248
                    diagnostics: diagnostics
1249
                });
1250
            }
1251
        });
1252
    }
1253

1254
    @AddStackToErrorMessage
1255
    public async onExecuteCommand(params: ExecuteCommandParams) {
1✔
1256
        await this.waitAllProjectFirstRuns();
2✔
1257
        if (params.command === CustomCommands.TranspileFile) {
2!
1258
            const result = await this.transpileFile(params.arguments[0]);
2✔
1259
            //back-compat: include `pathAbsolute` property so older vscode versions still work
1260
            (result as any).pathAbsolute = result.srcPath;
2✔
1261
            return result;
2✔
1262
        }
1263
    }
1264

1265
    private async transpileFile(srcPath: string) {
1266
        //wait all program first runs
1267
        await this.waitAllProjectFirstRuns();
2✔
1268
        //find the first project that has this file
1269
        for (let project of this.getProjects()) {
2✔
1270
            if (project.builder.program.hasFile(srcPath)) {
2!
1271
                return project.builder.program.getTranspiledFileContents(srcPath);
2✔
1272
            }
1273
        }
1274
    }
1275

1276
    public dispose() {
1277
        this.loggerSubscription?.();
34✔
1278
        this.validateThrottler.dispose();
34✔
1279
    }
1280
}
1281

1282
export interface Project {
1283
    firstRunPromise: Promise<any>;
1284
    builder: ProgramBuilder;
1285
    /**
1286
     * The path to where the project resides
1287
     */
1288
    projectPath: string;
1289
    /**
1290
     * The path to the workspace where this project resides. A workspace can have multiple projects (by adding a bsconfig.json to each folder).
1291
     */
1292
    workspacePath: string;
1293
    isFirstRunComplete: boolean;
1294
    isFirstRunSuccessful: boolean;
1295
    configFilePath?: string;
1296
    isStandaloneFileProject: boolean;
1297
}
1298

1299
export enum CustomCommands {
1✔
1300
    TranspileFile = 'TranspileFile'
1✔
1301
}
1302

1303
/**
1304
 * Wraps a method. If there's an error (either sync or via a promise),
1305
 * this appends the error's stack trace at the end of the error message so that the connection will
1306
 */
1307
function AddStackToErrorMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
1308
    let originalMethod = descriptor.value;
17✔
1309

1310
    //wrapping the original method
1311
    descriptor.value = function value(...args: any[]) {
17✔
1312
        try {
29✔
1313
            let result = originalMethod.apply(this, args);
29✔
1314
            //if the result looks like a promise, log if there's a rejection
1315
            if (result?.then) {
29!
1316
                return Promise.resolve(result).catch((e: Error) => {
27✔
1317
                    if (e?.stack) {
×
1318
                        e.message = e.stack;
×
1319
                    }
1320
                    return Promise.reject(e);
×
1321
                });
1322
            } else {
1323
                return result;
2✔
1324
            }
1325
        } catch (e: any) {
1326
            if (e?.stack) {
×
1327
                e.message = e.stack;
×
1328
            }
1329
            throw e;
×
1330
        }
1331
    };
1332
}
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