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

rokucommunity / brighterscript / #9776

pending completion
#9776

push

web-flow
Fix crash when func has no block (#774)

5604 of 6850 branches covered (81.81%)

Branch coverage included in aggregate %.

8349 of 9016 relevant lines covered (92.6%)

1584.89 hits per line

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

55.96
/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;
35✔
52

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

55
    /**
56
     * The number of milliseconds that should be used for language server typing debouncing
57
     */
58
    private debounceTimeout = 150;
35✔
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>;
35✔
67

68
    private hasConfigurationCapability = false;
35✔
69

70
    /**
71
     * Indicates whether the client supports workspace folders
72
     */
73
    private clientHasWorkspaceFolderCapability = false;
35✔
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);
35✔
80

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

85
    private loggerSubscription: () => void;
86

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

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

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

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

95
    private validateAllThrottled() {
96
        return this.validateThrottler.run(this.boundValidateAll);
4✔
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();
14✔
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) => {
14✔
107
            this.connection.tracer.log(text);
96✔
108
        });
109

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

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

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

116
        this.connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); //eslint-disable-line
14✔
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));
14✔
123

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

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

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

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

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

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

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

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

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

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

148
        this.connection.onCodeAction(this.onCodeAction.bind(this));
14✔
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));
14✔
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);
14✔
175

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

180
    /**
181
     * Called when the client starts initialization
182
     */
183
    @AddStackToErrorMessage
184
    public onInitialize(params: InitializeParams) {
1✔
185
        let clientCapabilities = params.capabilities;
2✔
186

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

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

227
    private initialProjectsCreated: Promise<any>;
228

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

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

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

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

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

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

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

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

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

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

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

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

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

380
            await this.syncProjects();
1✔
381

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

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

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

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

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

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

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

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

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

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

483
        //no config file could be found
484
        return undefined;
25✔
485
    }
486

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

495
        let builder = new ProgramBuilder();
32✔
496

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

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

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

511
        let configFilePath = await this.getConfigFilePath(projectPath);
32✔
512

513
        let cwd = projectPath;
32✔
514

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

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

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

547
        this.projects.push(newProject);
32✔
548

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

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

573
        let builder = new ProgramBuilder();
2✔
574

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

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

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

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

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

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

624
        this.standaloneFileProjects[srcPath] = newProject;
2✔
625

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

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

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

652
        let filePath = util.uriToPath(params.textDocument.uri);
×
653

654
        //wait until the file has settled
655
        await this.keyedThrottler.onIdleOnce(filePath, true);
×
656

657
        let completions = this
×
658
            .getProjects()
659
            .flatMap(workspace => workspace.builder.program.getCompletions(filePath, params.position));
×
660

661
        for (let completion of completions) {
×
662
            completion.commitCharacters = ['.'];
×
663
        }
664

665
        return completions;
×
666
    }
667

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

683
    @AddStackToErrorMessage
684
    private async onCodeAction(params: CodeActionParams) {
1✔
685
        //ensure programs are initialized
686
        await this.waitAllProjectFirstRuns();
×
687

688
        let srcPath = util.uriToPath(params.textDocument.uri);
×
689

690
        //wait until the file has settled
691
        await this.keyedThrottler.onIdleOnce(srcPath, true);
×
692

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

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

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

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

730
                //handle standard workspace
731
                if (project.isStandaloneFileProject === false) {
×
732
                    this.removeProject(project);
×
733

734
                    //create a new workspace/brs program
735
                    await this.createProject(project.projectPath, project.workspacePath);
×
736

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

749
            // valdiate all workspaces
750
            this.validateAllThrottled(); //eslint-disable-line
×
751
        }
752
    }
753

754
    private getRootDir(workspace: Project) {
755
        let options = workspace?.builder?.program?.options;
×
756
        return options?.rootDir ?? options?.cwd;
×
757
    }
758

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

768
        //remove standalone workspaces that are now included in projects
769
        for (let standaloneFilePath in this.standaloneFileProjects) {
4✔
770
            let standaloneProject = this.standaloneFileProjects[standaloneFilePath];
×
771
            for (let project of this.projects) {
×
772
                await standaloneProject.firstRunPromise;
×
773

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

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

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

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

833
        this.connection.sendNotification('build-status', 'building');
4✔
834

835
        let projects = this.getProjects();
4✔
836

837
        //convert all file paths to absolute paths
838
        let changes = params.changes.map(x => {
4✔
839
            return {
5✔
840
                type: x.type,
841
                srcPath: s`${URI.parse(x.uri).fsPath}`
842
            };
843
        });
844

845
        let keys = changes.map(x => x.srcPath);
5✔
846

847
        //filter the list of changes to only the ones that made it through the debounce unscathed
848
        changes = changes.filter(x => keys.includes(x.srcPath));
5✔
849

850
        //if we have changes to work with
851
        if (changes.length > 0) {
4!
852

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

858
            //reload any workspace whose bsconfig.json file has changed
859
            {
860
                let projectsToReload = [] as Project[];
4✔
861
                //get the file paths as a string array
862
                let filePaths = changes.map((x) => x.srcPath);
5✔
863

864
                for (let project of projects) {
4✔
865
                    if (project.configFilePath && filePaths.includes(project.configFilePath)) {
4!
866
                        projectsToReload.push(project);
×
867
                    }
868
                }
869
                if (projectsToReload.length > 0) {
4!
870
                    //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
871
                    //reload any projects that need to be reloaded
872
                    await this.reloadProjects(projectsToReload);
×
873
                }
874

875
                //reassign `projects` to the non-reloaded projects
876
                projects = projects.filter(x => !projectsToReload.includes(x));
4✔
877
            }
878

879
            //convert created folders into a list of files of their contents
880
            const directoryChanges = changes
4✔
881
                //get only creation items
882
                .filter(change => change.type === FileChangeType.Created)
5✔
883
                //keep only the directories
884
                .filter(change => util.isDirectorySync(change.srcPath));
3✔
885

886
            //remove the created directories from the changes array (we will add back each of their files next)
887
            changes = changes.filter(x => !directoryChanges.includes(x));
5✔
888

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

910
            //add the new file changes to the changes array.
911
            changes.push(...newFileChanges as any);
4✔
912

913
            //give every workspace the chance to handle file changes
914
            await Promise.all(
4✔
915
                projects.map((project) => this.handleFileChanges(project, changes))
4✔
916
            );
917
        }
918
        this.connection.sendNotification('build-status', 'success');
4✔
919
    }
920

921
    /**
922
     * This only operates on files that match the specified files globs, so it is safe to throw
923
     * any file changes you receive with no unexpected side-effects
924
     */
925
    public async handleFileChanges(project: Project, changes: { type: FileChangeType; srcPath: string }[]) {
926
        //this loop assumes paths are both file paths and folder paths, which eliminates the need to detect.
927
        //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
928
        let consumeCount = 0;
6✔
929
        await Promise.all(changes.map(async (change) => {
6✔
930
            await this.keyedThrottler.run(change.srcPath, async () => {
10✔
931
                consumeCount += await this.handleFileChange(project, change) ? 1 : 0;
10✔
932
            });
933
        }));
934

935
        if (consumeCount > 0) {
6✔
936
            await this.validateAllThrottled();
4✔
937
        }
938
    }
939

940
    /**
941
     * This only operates on files that match the specified files globs, so it is safe to throw
942
     * any file changes you receive with no unexpected side-effects
943
     */
944
    private async handleFileChange(project: Project, change: { type: FileChangeType; srcPath: string }) {
945
        const { program, options, rootDir } = project.builder;
10✔
946

947
        //deleted
948
        if (change.type === FileChangeType.Deleted) {
10!
949
            //try to act on this path as a directory
950
            project.builder.removeFilesInFolder(change.srcPath);
×
951

952
            //if this is a file loaded in the program, remove it
953
            if (program.hasFile(change.srcPath)) {
×
954
                program.removeFile(change.srcPath);
×
955
                return true;
×
956
            } else {
957
                return false;
×
958
            }
959

960
            //created
961
        } else if (change.type === FileChangeType.Created) {
10✔
962
            // thanks to `onDidChangeWatchedFiles`, we can safely assume that all "Created" changes are file paths, (not directories)
963

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

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

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

1001
    @AddStackToErrorMessage
1002
    private async onHover(params: TextDocumentPositionParams) {
1✔
1003
        //ensure programs are initialized
1004
        await this.waitAllProjectFirstRuns();
×
1005

1006
        const srcPath = util.uriToPath(params.textDocument.uri);
×
1007
        let projects = this.getProjects();
×
1008
        let hovers = projects
×
1009
            //get hovers from all projects
1010
            .map((x) => x.builder.program.getHover(srcPath, params.position))
×
1011
            //flatten to a single list
1012
            .flat();
1013

1014
        const contents = [
×
1015
            ...(hovers ?? [])
×
1016
                //pull all hover contents out into a flag array of strings
1017
                .map(x => {
1018
                    return Array.isArray(x?.contents) ? x?.contents : [x?.contents];
×
1019
                }).flat()
1020
                //remove nulls
1021
                .filter(x => !!x)
×
1022
                //dedupe hovers across all projects
1023
                .reduce((set, content) => set.add(content), new Set<string>()).values()
×
1024
        ];
1025

1026
        if (contents.length > 0) {
×
1027
            let hover: Hover = {
×
1028
                //use the range from the first hover
1029
                range: hovers[0]?.range,
×
1030
                //the contents of all hovers
1031
                contents: contents
1032
            };
1033
            return hover;
×
1034
        }
1035
    }
1036

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

1051
    @AddStackToErrorMessage
1052
    private async validateTextDocument(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
1✔
1053
        const { document } = event;
×
1054
        //ensure programs are initialized
1055
        await this.waitAllProjectFirstRuns();
×
1056

1057
        let filePath = URI.parse(document.uri).fsPath;
×
1058

1059
        try {
×
1060

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

1064
                this.connection.sendNotification('build-status', 'building');
×
1065

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

1087
    private async validateAll() {
1088
        try {
4✔
1089
            //synchronize parsing for open files that were included/excluded from projects
1090
            await this.synchronizeStandaloneProjects();
4✔
1091

1092
            let projects = this.getProjects();
4✔
1093

1094
            //validate all programs
1095
            await Promise.all(
4✔
1096
                projects.map((x) => x.builder.program.validate())
3✔
1097
            );
1098
        } catch (e: any) {
1099
            this.connection.console.error(e);
×
1100
            this.sendCriticalFailure(`Critical error validating project: ${e.message}${e.stack ?? ''}`);
×
1101
        }
1102

1103
        this.connection.sendNotification('build-status', 'success');
4✔
1104
    }
1105

1106
    @AddStackToErrorMessage
1107
    public async onWorkspaceSymbol(params: WorkspaceSymbolParams) {
1✔
1108
        await this.waitAllProjectFirstRuns();
4✔
1109

1110
        const results = util.flatMap(
4✔
1111
            await Promise.all(this.getProjects().map(project => {
1112
                return project.builder.program.getWorkspaceSymbols();
4✔
1113
            })),
1114
            c => c
4✔
1115
        );
1116

1117
        // Remove duplicates
1118
        const allSymbols = Object.values(results.reduce((map, symbol) => {
4✔
1119
            const key = symbol.location.uri + symbol.name;
24✔
1120
            map[key] = symbol;
24✔
1121
            return map;
24✔
1122
        }, {}));
1123
        return allSymbols as SymbolInformation[];
4✔
1124
    }
1125

1126
    @AddStackToErrorMessage
1127
    public async onDocumentSymbol(params: DocumentSymbolParams) {
1✔
1128
        await this.waitAllProjectFirstRuns();
6✔
1129

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

1132
        const srcPath = util.uriToPath(params.textDocument.uri);
6✔
1133
        for (const project of this.getProjects()) {
6✔
1134
            const file = project.builder.program.getFile(srcPath);
6✔
1135
            if (isBrsFile(file)) {
6!
1136
                return file.getDocumentSymbols();
6✔
1137
            }
1138
        }
1139
    }
1140

1141
    @AddStackToErrorMessage
1142
    private async onDefinition(params: TextDocumentPositionParams) {
1✔
1143
        await this.waitAllProjectFirstRuns();
5✔
1144

1145
        const srcPath = util.uriToPath(params.textDocument.uri);
5✔
1146

1147
        const results = util.flatMap(
5✔
1148
            await Promise.all(this.getProjects().map(project => {
1149
                return project.builder.program.getDefinition(srcPath, params.position);
5✔
1150
            })),
1151
            c => c
5✔
1152
        );
1153
        return results;
5✔
1154
    }
1155

1156
    @AddStackToErrorMessage
1157
    private async onSignatureHelp(params: SignatureHelpParams) {
1✔
1158
        await this.waitAllProjectFirstRuns();
3✔
1159

1160
        const filepath = util.uriToPath(params.textDocument.uri);
3✔
1161
        await this.keyedThrottler.onIdleOnce(filepath, true);
3✔
1162

1163
        try {
3✔
1164
            const signatures = util.flatMap(
3✔
1165
                await Promise.all(this.getProjects().map(project => project.builder.program.getSignatureHelp(filepath, params.position)
3✔
1166
                )),
1167
                c => c
3✔
1168
            );
1169

1170
            const activeSignature = signatures.length > 0 ? 0 : null;
3!
1171

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

1174
            let results: SignatureHelp = {
3✔
1175
                signatures: signatures.map((s) => s.signature),
3✔
1176
                activeSignature: activeSignature,
1177
                activeParameter: activeParameter
1178
            };
1179
            return results;
3✔
1180
        } catch (e: any) {
1181
            this.connection.console.error(`error in onSignatureHelp: ${e.stack ?? e.message ?? e}`);
×
1182
            return {
×
1183
                signatures: [],
1184
                activeSignature: 0,
1185
                activeParameter: 0
1186
            };
1187
        }
1188
    }
1189

1190
    @AddStackToErrorMessage
1191
    private async onReferences(params: ReferenceParams) {
1✔
1192
        await this.waitAllProjectFirstRuns();
3✔
1193

1194
        const position = params.position;
3✔
1195
        const srcPath = util.uriToPath(params.textDocument.uri);
3✔
1196

1197
        const results = util.flatMap(
3✔
1198
            await Promise.all(this.getProjects().map(project => {
1199
                return project.builder.program.getReferences(srcPath, position);
3✔
1200
            })),
1201
            c => c
3✔
1202
        );
1203
        return results.filter((r) => r);
5✔
1204
    }
1205

1206
    @AddStackToErrorMessage
1207
    private async onFullSemanticTokens(params: SemanticTokensParams) {
1✔
1208
        await this.waitAllProjectFirstRuns();
1✔
1209
        await Promise.all([
1✔
1210
            //wait for the file to settle (in case there are multiple file changes in quick succession)
1211
            this.keyedThrottler.onIdleOnce(util.uriToPath(params.textDocument.uri), true),
1212
            // wait for the validation to finish before providing semantic tokens. program.validate() populates and then caches AstNode.parent properties.
1213
            // If we don't wait, then fetching semantic tokens can cause some invalid cache
1214
            this.validateThrottler.onIdleOnce(false)
1215
        ]);
1216

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

1229
    private diagnosticCollection = new DiagnosticCollection();
35✔
1230

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc