diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index 083644c..171accb 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -18,7 +18,7 @@ import { DefaultFiles, rokuDeploy } from 'roku-deploy'; import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; import { RendezvousTracker } from '../RendezvousTracker'; -import { ClientToServerCustomEventName, isCustomRequestEvent } from './Events'; +import { ClientToServerCustomEventName, isCustomRequestEvent, LogOutputEvent } from './Events'; import { EventEmitter } from 'eventemitter3'; const sinon = sinonActual.createSandbox(); @@ -1164,5 +1164,172 @@ describe('BrightScriptDebugSession', () => { expect(getVarStub.calledWith('person.name', frameId, true)); }); }); + + describe('sendLogOutput', () => { + + async function doTest(input: string, output: string, locations: Array<{ filePath: string; lineNumber: number; columnIndex?: number }>) { + const getSourceLocationStub = sinon.stub(session.projectManager, 'getSourceLocation').callsFake(() => { + return Promise.resolve(locations.shift() as any); + }); + + const sendEventStub = sinon.stub(session, 'sendEvent'); + + await session['sendLogOutput'](input); + + expect( + sendEventStub.getCalls().filter(x => x.args[0] instanceof LogOutputEvent).map(call => call.args[0].body.line).join('') + ).to.eql(output); + sendEventStub.restore(); + getSourceLocationStub.restore(); + } + + it('modifies pkg locations if found multiline', async () => { + await doTest( + `{ + backtrace: " + file/line: pkg:/components/services/NetworkBase.bs:251 + Function networkbase_runtaskthread() As Void + + file/line: pkg:/components/services/NetworkBase.bs:654 + Function networkbase_processresponse(message As Object) As Object + + file/line: pkg:/source/sgnode.bs:109 + Function sgnode_createnode(nodetype As String, fields As Dynamic) As Object" + message: "Divide by Zero." + number: 20 + rethrown: false + }`, + `{ + backtrace: " + file/line: file://c:/project/components/services/NetworkBase.bs:260 + Function networkbase_runtaskthread() As Void + + file/line: file://c:/project/components/services/NetworkBase.bs:700 + Function networkbase_processresponse(message As Object) As Object + + file/line: file://c:/project/components/services/sgnode.bs:100 + Function sgnode_createnode(nodetype As String, fields As Dynamic) As Object" + message: "Divide by Zero." + number: 20 + rethrown: false + }`, + [ + { filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260 }, + { filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 700 }, + { filePath: `c:/project/components/services/sgnode.bs`, lineNumber: 100 } + ] + ); + }); + + it('modifies windows pkg locations with just line', async () => { + await doTest( + ` pkg:/components/services/NetworkBase.bs:251`, + ` file://c:/project/components/services/NetworkBase.bs:260`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs(251)`, + ` file://C:/project/components/services/NetworkBase.bs:260`, + [{ filePath: `C:/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs:251`, + ` file://c:/project/components/services/NetworkBase.bs:260`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs(251)`, + ` file://c:/project/components/services/NetworkBase.bs:260`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + }); + + it('modifies windows pkg locations with line and column', async () => { + await doTest( + ` pkg:/components/services/NetworkBase.bs:251:10`, + ` file://c:/project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs(251:10)`, + ` file://C:/project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `C:/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs:251:10`, + ` file://c:/project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs(251:10)`, + ` file://c:/project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `c:/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + }); + + it('modifies mac/linx pkg locations with just line', async () => { + await doTest( + ` pkg:/components/services/NetworkBase.bs:251`, + ` file://project/components/services/NetworkBase.bs:260`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs(251)`, + ` file://project/components/services/NetworkBase.bs:260`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs:251`, + ` file://project/components/services/NetworkBase.bs:260`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs(251)`, + ` file://project/components/services/NetworkBase.bs:260`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260 }] + ); + }); + + it('modifies mac/linx pkg locations line and column', async () => { + await doTest( + ` pkg:/components/services/NetworkBase.bs:251:10`, + ` file://project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs(251:10)`, + ` file://project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs:251:10`, + ` file://project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + await doTest( + ` ...rvices/NetworkBase.bs(251:10)`, + ` file://project/components/services/NetworkBase.bs:260:12`, + [{ filePath: `/project/components/services/NetworkBase.bs`, lineNumber: 260, columnIndex: 11 }] + ); + }); + + it('does not modify path', async () => { + await doTest( + ` pkg:/components/services/NetworkBase.bs`, + ` pkg:/components/services/NetworkBase.bs`, + [undefined] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs:251`, + ` pkg:/components/services/NetworkBase.bs:251`, + [undefined] + ); + await doTest( + ` pkg:/components/services/NetworkBase.bs:251:10`, + ` pkg:/components/services/NetworkBase.bs:251:10`, + [undefined] + ); + }); + }); }); }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index ba0d708..fce746d 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -662,7 +662,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { } private pendingSendLogPromise = Promise.resolve(); - private deviceFilePathRegex = /(?:\s*)((?:\.\.\.|[A-Za-z_0-9]*pkg\:\/)[A-Za-z_0-9\/\.]+\.[A-Za-z_0-9\/]+\:(\d+))(?:\s*)/ig; /** * Send log output to the "client" (i.e. vscode) @@ -679,18 +678,22 @@ export class BrightScriptDebugSession extends BaseDebugSession { line += '\n'; } - let potentialPaths = line.matchAll(this.deviceFilePathRegex); + let potentialPaths = this.getPotentialPkgPaths(line); for (let potentialPath of potentialPaths) { - let originalLocation = await this.projectManager.getSourceLocation(potentialPath[1], parseInt(potentialPath[2])); + let originalLocation = await this.projectManager.getSourceLocation(potentialPath.path, potentialPath.lineNumber, potentialPath.columnNumber); if (originalLocation) { let replacement: string; if (originalLocation.filePath.startsWith('/')) { - replacement = `file:/${originalLocation.filePath}`; + replacement = `file:/${originalLocation.filePath}:${originalLocation.lineNumber}`; } else { - replacement = `file://${originalLocation.filePath}`; + replacement = `file://${originalLocation.filePath}:${originalLocation.lineNumber}`; } - line = line.replace(potentialPath[1], replacement); + if (potentialPath.columnNumber !== undefined) { + replacement += `:${originalLocation.columnIndex + 1}`; + } + + line = line.replaceAll(potentialPath.fullMatch, replacement); } } @@ -698,6 +701,46 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.sendEvent(new LogOutputEvent(line)); } }); + return this.pendingSendLogPromise; + } + + // https://regex101.com/r/ixpQiq/1 + private deviceFilePathRegex = /((?:\.\.\.|[A-Za-z_0-9]*pkg\:\/)[A-Za-z_0-9 \/\.]+\.[A-Za-z_0-9 \/]+)(?:(?:\:)(\d+)(?:\:(\d+))?|\((\d+)(?:\:(\d+))?\))/ig; + + /** + * Extracts potential package paths from a given line of text. + * + * This method uses a regular expression to find matches in the provided line + * and returns an array of objects containing details about each match. + * + * @param input - The line of text to search for potential package paths. + * @returns An array of objects, each containing: + * - `fullMatch`: The full matched string. + * - `path`: The extracted path from the match. + * - `lineNumber`: The line number extracted from the match. + * - `columnNumber`: The column number extracted from the match, or `undefined` if not found. + */ + private getPotentialPkgPaths(input: string): Array<{ fullMatch: string; path: string; lineNumber: number; columnNumber: number }> { + let matches = input.matchAll(this.deviceFilePathRegex); + let paths: ReturnType = []; + if (matches) { + for (let match of matches) { + let fullMatch = match[0]; + let path = match[1]; + let lineNumber = parseInt(match[2] ?? match[4]); + let columnNumber = parseInt(match[3] ?? match[5]); + if (isNaN(columnNumber)) { + columnNumber = undefined; + } + paths.push({ + fullMatch: fullMatch, + path: path, + lineNumber: lineNumber, + columnNumber: columnNumber + }); + } + } + return paths; } private async runAutomaticSceneGraphCommands(commands: string[]) {