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

rokucommunity / vscode-brightscript-language / #2721

08 Sep 2022 06:10PM UTC coverage: 41.691% (-0.2%) from 41.897%
#2721

push

TwitchBronBron
2.35.2

477 of 1427 branches covered (33.43%)

Branch coverage included in aggregate %.

1126 of 2418 relevant lines covered (46.57%)

7.32 hits per line

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

67.78
/src/LogOutputManager.ts
1
import * as vscode from 'vscode';
1✔
2
import type { DiagnosticCollection } from 'vscode';
3
import type { BrightScriptDebugCompileError } from 'roku-debug';
4
import type { DeclarationProvider } from './DeclarationProvider';
5
import type { LogDocumentLinkProvider } from './LogDocumentLinkProvider';
6
import { CustomDocumentLink } from './LogDocumentLinkProvider';
1✔
7
import * as fsExtra from 'fs-extra';
1✔
8
import type { BrightScriptLaunchConfiguration } from './DebugConfigurationProvider';
9

10
export class LogLine {
1✔
11
    constructor(
12
        public text: string,
65✔
13
        public isMustInclude: boolean
65✔
14
    ) {
15
    }
16
}
17

18
export class LogOutputManager {
1✔
19

20
    constructor(
21
        outputChannel,
22
        context,
23
        docLinkProvider,
24
        private declarationProvider: DeclarationProvider
41✔
25
    ) {
26
        this.collection = vscode.languages.createDiagnosticCollection('BrightScript');
41✔
27
        this.outputChannel = outputChannel;
41✔
28
        this.docLinkProvider = docLinkProvider;
41✔
29

30
        this.loadConfigSettings();
41✔
31
        vscode.workspace.onDidChangeConfiguration((e) => {
41✔
32
            this.loadConfigSettings();
×
33
        });
34

35
        this.context = context;
41✔
36
        let subscriptions = context.subscriptions;
41✔
37
        this.includeRegex = null;
41✔
38
        this.logLevelRegex = null;
41✔
39
        this.excludeRegex = null;
41✔
40
        /**
41
         * we want to catch a few different link formats here:
42
         *  - pkg:/path/file.brs(LINE:COL)
43
         *  - file://path/file.bs:LINE
44
         *  - at line LINE of file pkg:/path/file.brs - this case can arise when the device reports various scenegraph errors such as fields not present, or texture size issues, etc
45
         */
46
        this.pkgRegex = /(?:\s*at line (\d*) of file )*(?:(pkg:\/|file:\/\/)(.*\.(bs|brs|xml)))[ \t]*(?:(?:(?:\()(\d+)(?:\:(\d+))?\)?)|(?:\:(\d+)?))*/;
41✔
47
        this.debugStartRegex = /BrightScript Micro Debugger\./ig;
41✔
48
        this.debugEndRegex = /Brightscript Debugger>/ig;
41✔
49

50
        subscriptions.push(vscode.commands.registerCommand('extension.brightscript.markLogOutput', () => {
41✔
51
            this.markOutput();
×
52
        }));
53
        subscriptions.push(vscode.commands.registerCommand('extension.brightscript.clearLogOutput', () => {
41✔
54
            this.clearOutput();
×
55
        }));
56

57
        subscriptions.push(vscode.commands.registerCommand('extension.brightscript.setOutputIncludeFilter', async () => {
41✔
58
            let entryText: string = await vscode.window.showInputBox({
×
59
                placeHolder: 'Enter log include regex',
60
                value: this.includeRegex ? this.includeRegex.source : ''
×
61
            });
62
            if (entryText || entryText === '') {
×
63
                this.setIncludeFilter(entryText);
×
64
            }
65
        }));
66

67
        subscriptions.push(vscode.commands.registerCommand('extension.brightscript.setOutputLogLevelFilter', async () => {
41✔
68
            let entryText: string = await vscode.window.showInputBox({
×
69
                placeHolder: 'Enter log level regex',
70
                value: this.logLevelRegex ? this.logLevelRegex.source : ''
×
71
            });
72
            if (entryText || entryText === '') {
×
73
                this.setLevelFilter(entryText);
×
74
            }
75
        }));
76

77
        subscriptions.push(vscode.commands.registerCommand('extension.brightscript.setOutputExcludeFilter', async () => {
41✔
78
            let entryText: string = await vscode.window.showInputBox({
×
79
                placeHolder: 'Enter log exclude regex',
80
                value: this.excludeRegex ? this.excludeRegex.source : ''
×
81
            });
82
            if (entryText || entryText === '') {
×
83
                this.setExcludeFilter(entryText);
×
84
            }
85
        }));
86
        this.clearOutput();
41✔
87

88
    }
89
    private context: any;
90
    private displayedLogLines: LogLine[];
91
    private allLogLines: LogLine[];
92
    private markCount: number;
93
    private includeRegex?: RegExp;
94
    private logLevelRegex?: RegExp;
95
    private excludeRegex?: RegExp;
96
    private pkgRegex: RegExp;
97
    private isNextBreakpointSkipped = false;
41✔
98
    private includeStackTraces: boolean;
99
    private isInMicroDebugger: boolean;
100
    public launchConfig: BrightScriptLaunchConfiguration;
101
    public isFocusingOutputOnLaunch: boolean;
102
    public isClearingOutputOnLaunch: boolean;
103
    public isClearingConsoleOnChannelStart: boolean;
104
    public hyperlinkFormat: string;
105
    private collection: DiagnosticCollection;
106
    private outputChannel: vscode.OutputChannel;
107
    private docLinkProvider: LogDocumentLinkProvider;
108
    private debugStartRegex: RegExp;
109
    private debugEndRegex: RegExp;
110

111
    public onDidStartDebugSession() {
112
        this.isInMicroDebugger = false;
2✔
113
        this.isNextBreakpointSkipped = false;
2✔
114
        if (this.isClearingConsoleOnChannelStart) {
2✔
115
            this.clearOutput();
1✔
116
        }
117
    }
118

119
    private loadConfigSettings() {
120
        let config: any = vscode.workspace.getConfiguration('brightscript') || {};
41!
121
        this.includeStackTraces = config.output?.includeStackTraces;
41!
122
        this.isFocusingOutputOnLaunch = config?.output?.focusOnLaunch === false ? false : true;
41!
123
        this.isClearingOutputOnLaunch = config?.output?.clearOnLaunch === false ? false : true;
41!
124
        this.isClearingConsoleOnChannelStart = config?.output?.clearConsoleOnChannelStart === false ? false : true;
41!
125
        this.hyperlinkFormat = config.output?.hyperlinkFormat;
41!
126
    }
127

128
    public setLaunchConfig(launchConfig: BrightScriptLaunchConfiguration) {
129
        this.launchConfig = launchConfig;
×
130
    }
131

132
    public async onDidReceiveDebugSessionCustomEvent(e: { event: string; body?: any }) {
133
        if (e.event === 'BSRendezvousEvent' || e.event === 'BSChanperfEvent') {
6!
134
            // No need to handle rendezvous type events
135
            return;
×
136
        }
137

138
        if (e.event === 'BSLogOutputEvent') {
6✔
139
            this.appendLine(e.body);
1✔
140
        } else if (e.event === 'BSPopupMessageEvent') {
5!
141
            this.showMessage(e.body.message, e.body.severity);
×
142
        } else if (e.event === 'BSLaunchStartEvent') {
5✔
143
            this.isInMicroDebugger = false;
2✔
144
            this.isNextBreakpointSkipped = false;
2✔
145
            if (this.isFocusingOutputOnLaunch) {
2!
146
                await vscode.commands.executeCommand('workbench.action.focusPanel');
2✔
147
                this.outputChannel.show();
2✔
148
            }
149
            if (this.isClearingOutputOnLaunch) {
2✔
150
                this.clearOutput();
1✔
151
            }
152
        } else if (e.body && Array.isArray(e.body)) {
3✔
153
            let errorsByPath = {};
2✔
154
            for (const compileError of e.body) {
2✔
155
                if (compileError.path) {
1!
156
                    if (!errorsByPath[compileError.path]) {
1!
157
                        errorsByPath[compileError.path] = [];
1✔
158
                    }
159
                    errorsByPath[compileError.path].push(compileError);
1✔
160
                }
161
            }
162
            for (const path in errorsByPath) {
2✔
163
                if (errorsByPath.hasOwnProperty(path)) {
1!
164
                    const errors = errorsByPath[path];
1✔
165
                    await this.addDiagnosticForError(path, errors);
1✔
166
                }
167
            }
168
        }
169
    }
170

171
    private showMessage(message: string, severity: string) {
172
        const methods = {
×
173
            error: vscode.window.showErrorMessage,
174
            info: vscode.window.showInformationMessage,
175
            warn: vscode.window.showWarningMessage
176
        };
177
        methods[severity](message);
×
178
    }
179

180
    public async addDiagnosticForError(path: string, compileErrors: BrightScriptDebugCompileError[]) {
181

182
        //TODO get the actual folder
183
        let documentUri: vscode.Uri;
184
        let uri = vscode.Uri.file(path);
×
185
        let doc = await vscode.workspace.openTextDocument(uri); // calls back
×
186
        if (doc !== undefined) {
×
187
            documentUri = doc.uri;
×
188
        }
189
        // console.log("got " + documentUri);
190

191
        //debug crap - for some reason - using this URI works - using the one from the path does not :()
192
        // const document = vscode.window.activeTextEditor.document;
193
        // const currentDocumentUri = document.uri;
194
        // console.log("currentDocumentUri " + currentDocumentUri);
195
        if (documentUri !== undefined) {
×
196
            let diagnostics: vscode.Diagnostic[] = [];
×
197
            for (const compileError of compileErrors) {
×
198

199
                const path: string = compileError.path;
×
200
                const message: string = compileError.message;
×
201
                const source: string = compileError.errorText;
×
202
                const lineNumber: number = compileError.lineNumber;
×
203
                const charStart: number = compileError.charStart;
×
204
                const charEnd: number = compileError.charEnd;
×
205

206
                diagnostics.push({
×
207
                    code: '',
208
                    message: message,
209
                    range: new vscode.Range(new vscode.Position(lineNumber, charStart), new vscode.Position(lineNumber, charEnd)),
210
                    severity: vscode.DiagnosticSeverity.Error,
211
                    source: source
212
                });
213
            }
214
            this.collection.set(documentUri, diagnostics);
×
215
        }
216
    }
217

218
    /**
219
     * Log output methods
220
     */
221
    public appendLine(lineText: string, mustInclude = false): void {
1✔
222
        let lines = lineText.split(/\r?\n/g);
6✔
223
        for (let line of lines) {
6✔
224
            if (line !== '') {
7!
225
                if (!this.includeStackTraces) {
7!
226
                    // filter out debugger noise
227
                    if (this.debugStartRegex.exec(line)) {
7!
228
                        console.log('start MicroDebugger block');
×
229
                        this.isInMicroDebugger = true;
×
230
                        this.isNextBreakpointSkipped = false;
×
231
                        line = 'Pausing for a breakpoint...';
×
232
                    } else if (this.isInMicroDebugger && (this.debugEndRegex.exec(line))) {
7!
233
                        console.log('ended MicroDebugger block');
×
234
                        this.isInMicroDebugger = false;
×
235
                        if (this.isNextBreakpointSkipped) {
×
236
                            line = '\n**Was a bogus breakpoint** Skipping!\n';
×
237
                        } else {
238
                            line = null;
×
239
                        }
240
                    } else if (this.isInMicroDebugger) {
7!
241
                        if (this.launchConfig.enableDebuggerAutoRecovery && line.startsWith('Break in ')) {
×
242
                            console.log('this block is a break: skipping it');
×
243
                            this.isNextBreakpointSkipped = true;
×
244
                        }
245
                        line = null;
×
246
                    }
247
                }
248
            }
249
            if (line) {
7!
250
                const logLine = new LogLine(line, mustInclude);
7✔
251
                this.allLogLines.push(logLine);
7✔
252
                if (this.shouldLineBeShown(logLine)) {
7!
253
                    this.allLogLines.push(logLine);
7✔
254
                    this.addLogLineToOutput(logLine);
7✔
255
                    this.writeLogLineToLogfile(logLine.text);
7✔
256
                }
257
            }
258
        }
259
    }
260

261
    public writeLogLineToLogfile(text: string) {
262
        if (this.launchConfig?.logfilePath) {
7!
263
            fsExtra.appendFileSync(this.launchConfig.logfilePath, text + '\n');
×
264
        }
265
    }
266

267
    public addLogLineToOutput(logLine: LogLine) {
268
        const logLineNumber = this.displayedLogLines.length;
31✔
269
        if (this.shouldLineBeShown(logLine)) {
31!
270
            this.displayedLogLines.push(logLine);
31✔
271
            let match = this.pkgRegex.exec(logLine.text);
31✔
272
            if (match) {
31✔
273
                const isFilePath = match[2] === 'file://';
24✔
274
                const path = isFilePath ? match[3] : 'pkg:/' + match[3];
24✔
275
                let lineNumber = match[1] ? Number(match[1]) : undefined;
24!
276
                if (!lineNumber) {
24!
277
                    if (isFilePath) {
24✔
278
                        lineNumber = Number(match[7]);
3✔
279
                        if (isNaN(lineNumber)) {
3!
280
                            lineNumber = Number(match[5]);
3✔
281
                        }
282
                    } else {
283
                        lineNumber = Number(match[5]);
21✔
284
                    }
285
                }
286

287
                const filename = this.getFilename(path);
24✔
288
                const ext = `.${match[4]}`.toLowerCase();
24✔
289
                let customText = this.getCustomLogText(path, filename, ext, Number(lineNumber), logLineNumber, isFilePath);
24✔
290
                const customLink = new CustomDocumentLink(logLineNumber, match.index, customText.length, path, lineNumber, filename);
24✔
291
                if (isFilePath) {
24✔
292
                    this.docLinkProvider.addCustomFileLink(customLink);
3✔
293
                } else {
294
                    this.docLinkProvider.addCustomPkgLink(customLink);
21✔
295
                }
296
                let logText = logLine.text.substring(0, match.index) + customText + logLine.text.substring(match.index + match[0].length);
24✔
297
                this.outputChannel.appendLine(logText);
24✔
298
            } else {
299
                this.outputChannel.appendLine(logLine.text);
7✔
300
            }
301
        }
302
    }
303

304
    public getFilename(pkgPath: string): string {
305
        const parts = pkgPath.split('/');
34✔
306
        let name = parts.length > 0 ? parts[parts.length - 1] : pkgPath;
34!
307
        if (name.toLowerCase().endsWith('.xml') || name.toLowerCase().endsWith('.brs')) {
34✔
308
            name = name.substring(0, name.length - 4);
33✔
309
        } else if (name.toLowerCase().endsWith('.bs')) {
1!
310
            name = name.substring(0, name.length - 3);
×
311
        }
312
        return name;
34✔
313
    }
314

315
    public getCustomLogText(pkgPath: string, filename: string, extension: string, lineNumber: number, logLineNumber: number, isFilePath: boolean): string {
316
        switch (this.hyperlinkFormat) {
72✔
317
            case 'Full':
318
                return pkgPath + `(${lineNumber})`;
2✔
319
                break;
×
320
            case 'Short':
321
                return `#${logLineNumber}`;
2✔
322
                break;
×
323
            case 'Hidden':
324
                return ' ';
2✔
325
                break;
×
326
            case 'Filename':
327
                return `${filename}${extension}(${lineNumber})`;
12✔
328
                break;
×
329
            default:
330
                if (extension === '.brs' || extension === '.bs') {
54!
331
                    const methodName = this.getMethodName(pkgPath, lineNumber, isFilePath);
54✔
332
                    if (methodName) {
54!
333
                        return `${filename}.${methodName}(${lineNumber})`;
54✔
334
                    }
335
                }
336
                return pkgPath + `(${lineNumber})`;
×
337
                break;
×
338
        }
339
    }
340

341
    public getMethodName(path: string, lineNumber: number, isFilePath: boolean): string | null {
342
        let fsPath = isFilePath ? path : this.docLinkProvider.convertPkgPathToFsPath(path);
54✔
343
        const method = fsPath ? this.declarationProvider.getFunctionBeforeLine(fsPath, lineNumber) : null;
54!
344
        return method ? method.name : null;
54!
345
    }
346

347
    private shouldLineBeShown(logLine: LogLine): boolean {
348
        //filter excluded lines
349
        if (this.excludeRegex?.test(logLine.text)) {
72✔
350
            return false;
6✔
351
        }
352
        //once past the exclude filter, always keep "mustInclude" lines
353
        if (logLine.isMustInclude) {
66✔
354
            return true;
44✔
355
        }
356
        //throw out lines that don't match the logLevelRegex (if we have one)
357
        if (this.logLevelRegex && !this.logLevelRegex.test(logLine.text)) {
22✔
358
            return false;
6✔
359
        }
360
        //throw out lines that don't match the includeRegex (if we have one)
361
        if (this.includeRegex && !this.includeRegex.test(logLine.text)) {
16✔
362
            return false;
6✔
363
        }
364
        //all other log entries should be kept
365
        return true;
10✔
366
    }
367

368
    public clearOutput(): any {
369
        this.markCount = 0;
45✔
370
        this.allLogLines = [];
45✔
371
        this.displayedLogLines = [];
45✔
372
        this.outputChannel.clear();
45✔
373
        this.collection.clear();
45✔
374
        this.docLinkProvider.resetCustomLinks();
45✔
375
    }
376

377
    public setIncludeFilter(text: string): void {
378
        this.includeRegex = text && text.trim() !== '' ? new RegExp(text, 'i') : null;
35✔
379
        this.reFilterOutput();
35✔
380
    }
381

382
    public setExcludeFilter(text: string): void {
383
        this.excludeRegex = text && text.trim() !== '' ? new RegExp(text, 'i') : null;
35✔
384
        this.reFilterOutput();
35✔
385
    }
386

387
    public setLevelFilter(text: string): void {
388
        this.logLevelRegex = text && text.trim() !== '' ? new RegExp(text, 'i') : null;
35✔
389
        this.reFilterOutput();
35✔
390
    }
391

392
    public reFilterOutput(): void {
393
        this.outputChannel.clear();
102✔
394
        this.docLinkProvider.resetCustomLinks();
102✔
395

396
        for (let i = 0; i < this.allLogLines.length - 1; i++) {
102✔
397
            let logLine = this.allLogLines[i];
×
398
            if (this.shouldLineBeShown(logLine)) {
×
399
                this.addLogLineToOutput(logLine);
×
400
            }
401
        }
402
    }
403

404
    public markOutput(): void {
405
        this.appendLine(`---------------------- MARK ${this.markCount} ----------------------`, true);
8✔
406
        this.markCount++;
8✔
407
    }
408
}
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