diff --git a/package-lock.json b/package-lock.json index 0a9f01b..1dc4a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@rokucommunity/logger": "^0.3.9", "@types/request": "^2.48.8", - "brighterscript": "^0.67.2", + "brighterscript": "^0.67.3", + "clone-regexp": "2.2.0", "dateformat": "^4.6.3", "debounce": "^1.2.1", "eol": "^0.9.1", @@ -19,6 +20,7 @@ "fast-glob": "^3.2.11", "find-in-files": "^0.5.0", "fs-extra": "^10.0.0", + "line-column": "^1.0.2", "natural-orderby": "^2.0.3", "portfinder": "^1.0.32", "postman-request": "^2.88.1-postman.32", @@ -46,7 +48,7 @@ "@types/dedent": "^0.7.0", "@types/find-in-files": "^0.5.1", "@types/fs-extra": "^9.0.13", - "@types/glob": "^7.2.0", + "@types/line-column": "^1.0.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", "@types/request": "^2.48.8", @@ -71,7 +73,8 @@ "sinon": "^11.1.2", "source-map-support": "^0.5.20", "ts-node": "^10.4.0", - "typescript": "^4.7.2" + "typescript": "^4.4.4", + "undent": "^0.1.0" } }, "node_modules/@babel/code-frame": { @@ -852,26 +855,16 @@ "@types/node": "*" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "node_modules/@types/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", "dev": true }, "node_modules/@types/mocha": { @@ -1523,9 +1516,9 @@ } }, "node_modules/brighterscript": { - "version": "0.67.2", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.2.tgz", - "integrity": "sha512-JMDJvty/zk1XrvY/v4Ez8NoXausuH/wlSb5hXHkfj5+s6jYeCaquUehS6NgRoo7rVaU0iUW1rd6UGZFWK6jUjg==", + "version": "0.67.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.3.tgz", + "integrity": "sha512-uuAIvDmIENA+HdeuRiC1KM+n250m0J9k0Cdiwpby4cBZiFAHRkYoynEqmL8kwAaPtxdXI5RcZU4JqInCd1avlA==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@rokucommunity/logger": "^0.3.9", @@ -1556,6 +1549,7 @@ "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.0.8", "xml2js": "^0.5.0", "yargs": "^16.2.0" @@ -2017,6 +2011,17 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "dependencies": { + "is-regexp": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3503,6 +3508,14 @@ "node": ">=8" } }, + "node_modules/is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", + "engines": { + "node": ">=6" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3552,6 +3565,17 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3807,6 +3831,15 @@ "immediate": "~3.0.5" } }, + "node_modules/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=", + "dependencies": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -5759,6 +5792,12 @@ "through": "^2.3.8" } }, + "node_modules/undent": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/undent/-/undent-0.1.0.tgz", + "integrity": "sha512-vohX7ywgBjRxDNw+f3wHclSXmO0z9HsEfmGObOuG7G0yi7kZ6OtCG8kAxtDSNklmua5KR6ev2drTFqMGqpYEbg==", + "dev": true + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -6734,26 +6773,16 @@ "@types/node": "*" } }, - "@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "@types/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", "dev": true }, "@types/mocha": { @@ -7215,9 +7244,9 @@ } }, "brighterscript": { - "version": "0.67.2", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.2.tgz", - "integrity": "sha512-JMDJvty/zk1XrvY/v4Ez8NoXausuH/wlSb5hXHkfj5+s6jYeCaquUehS6NgRoo7rVaU0iUW1rd6UGZFWK6jUjg==", + "version": "0.67.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.3.tgz", + "integrity": "sha512-uuAIvDmIENA+HdeuRiC1KM+n250m0J9k0Cdiwpby4cBZiFAHRkYoynEqmL8kwAaPtxdXI5RcZU4JqInCd1avlA==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@rokucommunity/logger": "^0.3.9", @@ -7248,6 +7277,7 @@ "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.0.8", "xml2js": "^0.5.0", "yargs": "^16.2.0" @@ -7577,6 +7607,14 @@ "wrap-ansi": "^6.2.0" } }, + "clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "requires": { + "is-regexp": "^2.0.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8712,6 +8750,11 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8746,6 +8789,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8953,6 +9004,15 @@ "immediate": "~3.0.5" } }, + "line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=", + "requires": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -10425,6 +10485,12 @@ "through": "^2.3.8" } }, + "undent": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/undent/-/undent-0.1.0.tgz", + "integrity": "sha512-vohX7ywgBjRxDNw+f3wHclSXmO0z9HsEfmGObOuG7G0yi7kZ6OtCG8kAxtDSNklmua5KR6ev2drTFqMGqpYEbg==", + "dev": true + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 3abec3c..8bb2157 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@types/dedent": "^0.7.0", "@types/find-in-files": "^0.5.1", "@types/fs-extra": "^9.0.13", - "@types/glob": "^7.2.0", + "@types/line-column": "^1.0.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", "@types/request": "^2.48.8", @@ -93,12 +93,14 @@ "sinon": "^11.1.2", "source-map-support": "^0.5.20", "ts-node": "^10.4.0", - "typescript": "^4.7.2" + "typescript": "^4.4.4", + "undent": "^0.1.0" }, "dependencies": { "@rokucommunity/logger": "^0.3.9", "@types/request": "^2.48.8", - "brighterscript": "^0.67.2", + "brighterscript": "^0.67.3", + "clone-regexp": "2.2.0", "dateformat": "^4.6.3", "debounce": "^1.2.1", "eol": "^0.9.1", @@ -106,6 +108,7 @@ "fast-glob": "^3.2.11", "find-in-files": "^0.5.0", "fs-extra": "^10.0.0", + "line-column": "^1.0.2", "natural-orderby": "^2.0.3", "portfinder": "^1.0.32", "postman-request": "^2.88.1-postman.32", diff --git a/src/FileUtils.spec.ts b/src/FileUtils.spec.ts index fad9ace..4ee5ce3 100644 --- a/src/FileUtils.spec.ts +++ b/src/FileUtils.spec.ts @@ -2,30 +2,38 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; import * as sinonActual from 'sinon'; - import { SourceNode } from 'source-map'; -import { fileUtils, standardizePath as s } from './FileUtils'; +import { fileUtils } from './FileUtils'; +import { standardizePath as s } from 'brighterscript'; import { SourceMapManager } from './managers/SourceMapManager'; let sinon = sinonActual.createSandbox(); -const rootDir = path.normalize(path.dirname(__dirname)); +const tempDir = s`${__dirname}/../.tmp`; +const rootDir = s`${tempDir}/rootDir}`; describe('FileUtils', () => { let sourceMapManager: SourceMapManager; beforeEach(() => { + fsExtra.emptydirSync(tempDir); sourceMapManager = new SourceMapManager(); }); afterEach(() => { + fsExtra.removeSync(tempDir); sinon.restore(); }); describe('getAllRelativePaths', () => { //basic test to get code coverage...we don't need to test the glob code too much here... it('works', async () => { - let paths = await fileUtils.getAllRelativePaths(s`${__dirname}/../src/`); + fsExtra.outputFileSync(s`${tempDir}/file.txt`, ''); + fsExtra.outputFileSync(s`${tempDir}/subdir/file.txt`, ''); + let paths = await fileUtils.getAllRelativePaths(tempDir); expect( - paths - ).to.contain(`index.ts`); + paths.sort() + ).to.eql([ + 'file.txt', + s`subdir/file.txt` + ]); }); }); diff --git a/src/FileUtils.ts b/src/FileUtils.ts index 3a04d1f..2b9a297 100644 --- a/src/FileUtils.ts +++ b/src/FileUtils.ts @@ -1,10 +1,13 @@ -import * as findInFiles from 'find-in-files'; import * as fsExtra from 'fs-extra'; -import * as glob from 'glob'; +import * as fastGlob from 'fast-glob'; +import * as cloneRegexp from 'clone-regexp'; import * as path from 'path'; -import { promisify } from 'util'; -import { util as rokuDeployUtil } from 'roku-deploy'; -const globp = promisify(glob); +import * as rokuDeploy from 'roku-deploy'; +import type { Position, Range } from 'brighterscript'; +import { standardizePath as s, util } from 'brighterscript'; +import { Cache } from 'brighterscript/dist/Cache'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import lineColumn = require('line-column'); export class FileUtils { @@ -50,17 +53,12 @@ export class FileUtils { * @param directoryPath */ public async getAllRelativePaths(directoryPath: string) { - //normalize the path - directoryPath = this.removeTrailingSlash( - path.normalize(directoryPath) - ); - - let paths = await globp(path.join(directoryPath, '**/*')); - for (let i = 0; i < paths.length; i++) { - //make the path relative (+1 for removing the slash) - paths[i] = paths[i].substring(directoryPath.length + 1); - } - return paths; + let paths = await fastGlob('**/*', { + cwd: s`${directoryPath}` + }); + return paths + //os-normalize each path separator + .map(x => s`${x}`); } /** @@ -253,53 +251,91 @@ export class FileUtils { return result; } + /** + * Find all locations of the regex pattern in the specified files. + * @param pattern a regular expression pattern to find in all files. The global flag will be force-enabled before it's used. + */ + private async findInFiles(pattern: RegExp, cwd: string, fileGlobs: string[]) { + const filePaths = await fastGlob(fileGlobs, { + cwd: cwd, + absolute: true + }); + const results: Array<{ + srcPath: string; + range: Range; + fileContents: string; + match: RegExpExecArray; + }> = []; + await Promise.all(filePaths.map(async (filePath) => { + const fileContents = (await fsExtra.readFile(filePath)).toString(); + const finder = lineColumn(fileContents); + let match: RegExpExecArray; + const regexp = cloneRegexp(pattern, { global: true }); + while ((match = regexp.exec(fileContents))) { + const beginPosition = finder.fromIndex(match.index); + const endPosition = finder.fromIndex(match.index + match[0].length); + results.push({ + srcPath: s`${filePath}`, + //finder returns 1-based line and col values, so offset that in the Range + range: util.createRange( + beginPosition.line - 1, + beginPosition.col - 1, + endPosition.line - 1, + endPosition.col - 1 + ), + fileContents: fileContents, + match: match + }); + } + })); + results.sort((a, b) => a.srcPath.toLowerCase().localeCompare(b.srcPath.toLowerCase())); + return results; + } + /** * Given a path to a folder, search all files until an entry point is found. * (An entry point is a function that roku uses as the Main function to start the program). - * @param projectPath - a path to a Roku project + * @param rootDir - a path to the root of a Roku project. */ - public async findEntryPoint(projectPath: string) { - let results = { - ...await findInFiles.find({ term: 'sub\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'function\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'sub\\s+RunUserInterface\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'function\\s+RunUserInterface\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'sub\\s+main\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'function\\s+main\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/) - }; - let keys = Object.keys(results); - if (keys.length === 0) { - throw new Error('Unable to find an entry point. Please make sure that you have a RunUserInterface, RunScreenSaver, or Main sub/function declared in your BrightScript project'); - } - - let entryPath = keys[0]; - - let entryLineContents = results[entryPath].line[0]; + public findEntryPoint(rootDir: string) { + return this.entryPointCache.getOrAdd(this.standardizePath(rootDir), async (projectPath: string) => { + projectPath = s`${projectPath}`; + let searchResults = await this.findInFiles( + /(?:sub|function)\s+(RunUserInterface|main|RunScreenSaver)\s*\(/ig, + projectPath, + ['source/**/*.brs'] + ); - let lineNumber: number; - //load the file contents - let contents = await fsExtra.readFile(entryPath); - let lines = contents.toString().split(/\r?\n/g); - //loop through the lines until we find the entry line - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - if (line.includes(entryLineContents)) { - lineNumber = i + 1; - break; + if (searchResults.length === 0) { + throw new Error('Unable to find an entry point. Please make sure that you have a RunUserInterface, RunScreenSaver, or Main sub/function declared in your BrightScript project'); } - } - let relativePath = fileUtils.removeLeadingSlash( - rokuDeployUtil.stringReplaceInsensitive(entryPath, projectPath, '') - ); - return { - relativePath: relativePath, - pathAbsolute: entryPath, - contents: entryLineContents, - lineNumber: lineNumber - }; + const [firstResult] = searchResults; + + let destPath = fileUtils.removeLeadingSlash( + rokuDeploy.util.stringReplaceInsensitive(firstResult.srcPath, projectPath, '') + ); + return { + functionName: firstResult.match[1], + srcPath: firstResult.srcPath, + destPath: destPath, + fileContents: firstResult.fileContents, + position: firstResult.range.start + }; + }); } + private entryPointCache = new Cache>(); + /** * If a string has a leading slash, remove it */ diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index 21aa4c9..d2da91a 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -32,9 +32,14 @@ describe('BrightScriptDebugSession', () => { let responseDeferreds = []; let responses = []; + beforeEach(() => { + fsExtra.emptyDirSync(rootDir); + fsExtra.emptyDirSync(stagingDir); + fsExtra.emptyDirSync(outDir); + }); + afterEach(() => { - fsExtra.emptydirSync(tempDir); - fsExtra.removeSync(outDir); + fsExtra.emptyDirSync(tempDir); sinon.restore(); }); @@ -585,18 +590,15 @@ describe('BrightScriptDebugSession', () => { }); describe('findMainFunction', () => { - let folder; afterEach(() => { fsExtra.emptyDirSync('./.tmp'); fsExtra.rmdirSync('./.tmp'); }); - async function doTest(fileContents: string, lineContents: string, lineNumber: number) { - fsExtra.emptyDirSync('./.tmp'); - folder = path.resolve('./.tmp/findMainFunctionTests/'); - fsExtra.mkdirSync(folder); + async function doTest(fileContents: string, functionName: string, lineIndex: number) { + fsExtra.emptyDirSync(rootDir); - let filePath = path.resolve(`${folder}/main.brs`); + let filePath = path.resolve(`${rootDir}/source/main.brs`); //prevent actually talking to the file system...just hardcode the list to exactly our main file (session.rokuDeploy as any).getFilePaths = () => { @@ -606,56 +608,62 @@ describe('BrightScriptDebugSession', () => { }]; }; - fsExtra.writeFileSync(filePath, fileContents); + fsExtra.outputFileSync(filePath, fileContents); (session as any).launchConfiguration = { - files: [ - folder + '/**/*' - ] + rootDir: rootDir, + files: ['**/*'] }; - let entryPoint = await fileUtils.findEntryPoint(folder); - expect(entryPoint.pathAbsolute).to.equal(filePath); - expect(entryPoint.lineNumber).to.equal(lineNumber); - expect(entryPoint.contents).to.equal(lineContents); + //clear the cache so tests work + fileUtils['entryPointCache'].clear(); + let entryPoint = await fileUtils.findEntryPoint(rootDir); + expect(s`${entryPoint.srcPath}`).to.equal(s`${filePath}`); + expect(entryPoint.position.line).to.equal(lineIndex); + expect(entryPoint.functionName).to.eql(functionName); + return entryPoint; } it('works for RunUserInterface', async () => { - await doTest('\nsub RunUserInterface()\nend sub', 'sub RunUserInterface()', 2); + await doTest('\nsub RunUserInterface()\nend sub', 'RunUserInterface', 1); //works with args - await doTest('\n\nsub RunUserInterface(args as Dynamic)\nend sub', 'sub RunUserInterface(args as Dynamic)', 3); + await doTest('\n\nsub RunUserInterface(args as Dynamic)\nend sub', 'RunUserInterface', 2); //works with extra spacing - await doTest('\n\nsub RunUserInterface()\nend sub', 'sub RunUserInterface()', 3); - await doTest('\n\nsub RunUserInterface ()\nend sub', 'sub RunUserInterface ()', 3); + await doTest('\n\nsub RunUserInterface()\nend sub', 'RunUserInterface', 2); + await doTest('\n\nsub RunUserInterface ()\nend sub', 'RunUserInterface', 2); }); it('works for sub main', async () => { - await doTest('\nsub Main()\nend sub', 'sub Main()', 2); + await doTest('\nsub Main()\nend sub', 'Main', 1); //works with args - await doTest('sub Main(args as Dynamic)\nend sub', 'sub Main(args as Dynamic)', 1); + await doTest('sub Main(args as Dynamic)\nend sub', 'Main', 0); //works with extra spacing - await doTest('sub Main()\nend sub', 'sub Main()', 1); - await doTest('sub Main ()\nend sub', 'sub Main ()', 1); + await doTest('sub Main()\nend sub', 'Main', 0); + await doTest('sub Main ()\nend sub', 'Main', 0); + }); + + it('picks the top main function when there are multiples, and returns the name of the function', async () => { + await doTest(`sub main()\nend sub\nsub main()\nend sub`, `main`, 0); }); it('works for function main', async () => { - await doTest('function Main()\nend function', 'function Main()', 1); - await doTest('function Main(args as Dynamic)\nend function', 'function Main(args as Dynamic)', 1); + await doTest('function Main()\nend function', 'Main', 0); + await doTest('function Main(args as Dynamic)\nend function', 'Main', 0); //works with extra spacing - await doTest('function Main()\nend function', 'function Main()', 1); - await doTest('function Main ()\nend function', 'function Main ()', 1); + await doTest('function Main()\nend function', 'Main', 0); + await doTest('function Main ()\nend function', 'Main', 0); }); it('works for sub RunScreenSaver', async () => { - await doTest('sub RunScreenSaver()\nend sub', 'sub RunScreenSaver()', 1); + await doTest('sub RunScreenSaver()\nend sub', 'RunScreenSaver', 0); //works with extra spacing - await doTest('sub RunScreenSaver()\nend sub', 'sub RunScreenSaver()', 1); - await doTest('sub RunScreenSaver ()\nend sub', 'sub RunScreenSaver ()', 1); + await doTest('sub RunScreenSaver()\nend sub', 'RunScreenSaver', 0); + await doTest('sub RunScreenSaver ()\nend sub', 'RunScreenSaver', 0); }); it('works for function RunScreenSaver', async () => { - await doTest('function RunScreenSaver()\nend function', 'function RunScreenSaver()', 1); + await doTest('function RunScreenSaver()\nend function', 'RunScreenSaver', 0); //works with extra spacing - await doTest('function RunScreenSaver()\nend function', 'function RunScreenSaver()', 1); - await doTest('function RunScreenSaver ()\nend function', 'function RunScreenSaver ()', 1); + await doTest('function RunScreenSaver()\nend function', 'RunScreenSaver', 0); + await doTest('function RunScreenSaver ()\nend function', 'RunScreenSaver', 0); }); }); diff --git a/src/managers/LocationManager.ts b/src/managers/LocationManager.ts index e043389..5a807ec 100644 --- a/src/managers/LocationManager.ts +++ b/src/managers/LocationManager.ts @@ -2,7 +2,7 @@ import * as fsExtra from 'fs-extra'; import * as path from 'path'; import { standardizePath as s, fileUtils } from '../FileUtils'; import type { SourceMapManager } from './SourceMapManager'; -import * as glob from 'glob'; +import * as fastGlob from 'fast-glob'; /** * Find original source locations based on debugger/staging locations. @@ -114,7 +114,7 @@ export class LocationManager { //look through the sourcemaps in the staging folder for any instances of this source location let locations = await this.sourceMapManager.getGeneratedLocations( - glob.sync('**/*.map', { + await fastGlob('**/*.map', { cwd: stagingDir, absolute: true }), diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index ab9579f..6269897 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -809,4 +809,65 @@ describe('ComponentLibraryProject', () => { }); }); }); + + describe('removeFileNamePostfix', () => { + let project: ComponentLibraryProject; + beforeEach(() => { + project = new ComponentLibraryProject(params); + }); + + it('removes postfix from paths that contain it', () => { + expect(project.removeFileNamePostfix(`source/main__lib0.brs`)).to.equal('source/main.brs'); + expect(project.removeFileNamePostfix(`components/component1__lib0.brs`)).to.equal('components/component1.brs'); + }); + + it('removes postfix case insensitive', () => { + expect(project.removeFileNamePostfix(`source/main__LIB0.brs`)).to.equal('source/main.brs'); + expect(project.removeFileNamePostfix(`source/MAIN__lib0.brs`)).to.equal('source/MAIN.brs'); + }); + + it('does nothing to files without the postfix', () => { + expect(project.removeFileNamePostfix(`source/main.brs`)).to.equal('source/main.brs'); + }); + + it('does nothing to files with a different postfix', () => { + expect(project.removeFileNamePostfix(`source/main__lib1.brs`)).to.equal('source/main__lib1.brs'); + }); + + it('only removes the postfix from the end of the file', () => { + expect(project.removeFileNamePostfix(`source/__lib1.brs/main.brs`)).to.equal('source/__lib1.brs/main.brs'); + }); + }); + + describe('findSceneShow', () => { + it('finds simple case', () => { + fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ` + sub Main(inputArguments as object) + screen = createObject("roSGScreen") + m.port = createObject("roMessagePort") + screen.setMessagePort(m.port) + scene = screen.CreateScene("MainScene") + screen.show() : initThing() + scene.observeField("appExit", m.port) + scene.setFocus(true) + + while true + msg = wait(0, m.port) + msgType = type(msg) + + if msgType = "roSGScreenEvent" then + if msg.isScreenClosed() then + return + else if msgType = "roSGNodeEvent" then + field = msg.getField() + if field = "appExit" then + return + end if + end if + end if + end while + end sub + `); + }); + }); }); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 7eb544e..148923c 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -3,15 +3,16 @@ import * as fsExtra from 'fs-extra'; import * as path from 'path'; import { rokuDeploy, RokuDeploy, util as rokuDeployUtil } from 'roku-deploy'; import type { FileEntry } from 'roku-deploy'; -import * as glob from 'glob'; -import { promisify } from 'util'; -const globAsync = promisify(glob); +import * as fastGlob from 'fast-glob'; import type { BreakpointManager } from './BreakpointManager'; import { fileUtils, standardizePath as s } from '../FileUtils'; import type { LocationManager, SourceLocation } from './LocationManager'; import { util } from '../util'; import { logger } from '../logging'; +import { AssignmentStatement, CancellationTokenSource, DottedSetStatement, FunctionStatement, IndexedSetStatement, isAssignmentStatement, isCallExpression, isDottedGetExpression, isDottedSetStatement, isFunctionStatement, isIndexedSetStatement, isVariableExpression, Parser, WalkMode } from 'brighterscript'; +import lineColumn = require('line-column'); import { Cache } from 'brighterscript/dist/Cache'; +import { util as bscUtil } from 'brighterscript'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const replaceInFile = require('replace-in-file'); @@ -159,9 +160,9 @@ export class ProjectManager { let entryPoint = await fileUtils.findEntryPoint(stagingDir); //convert entry point staging location to source location - let sourceLocation = await this.getSourceLocation(entryPoint.relativePath, entryPoint.lineNumber); + let sourceLocation = await this.getSourceLocation(entryPoint.destPath, entryPoint.position.line + 1); - this.logger.info(`Registering entry breakpoint at ${sourceLocation.filePath}:${sourceLocation.lineNumber} (${entryPoint.pathAbsolute}:${entryPoint.lineNumber})`); + this.logger.info(`Registering entry breakpoint at ${sourceLocation.filePath}:${sourceLocation.lineNumber} (${entryPoint.srcPath}:${entryPoint.position.line + 1})`); //register the entry breakpoint this.breakpointManager.setBreakpoint(sourceLocation.filePath, { //+1 to select the first line of the function @@ -416,6 +417,94 @@ export class Project { } } + /** + * Find the line where the `scene.show()` function is called (if possible). + * This does the following: + * - finds the entryPoint function + * - scan the function to find the `screen = createObject("roSGScreen")` call and note its variable + * - find where the screen variable's `show()` method is called (i.e. `screen.show()`) + */ + private async findSceneShow() { + const entryPoint = await fileUtils.findEntryPoint(this.rootDir); + const lowerFunctionName = entryPoint.functionName?.toLowerCase(); + const { ast } = Parser.parse( + entryPoint.fileContents + ); + + const entryFunc = ast.findChild(node => { + return isFunctionStatement(node) && node.name.text.toLowerCase() === lowerFunctionName; + }, { + walkMode: WalkMode.visitStatements + }); + + if (!entryFunc) { + return; + } + + //find the "screen.createScene()" call + const cancel = new CancellationTokenSource(); + const sceneVar = entryFunc.findChild((node) => { + //find a function call in this format: `something.CreateScene(` + if (isCallExpression(node) && isDottedGetExpression(node.callee) && node.callee.name.text?.toLowerCase() === 'createscene') { + //walk upwards until we find an assignment, dotted set, or indexed set + const result = node.findAncestor((ancestor) => { + if (isAssignmentStatement(ancestor)) { + return true; + } + }) as AssignmentStatement; + + if (result) { + return result; + } else { + cancel.cancel(); + } + } + }, { + walkMode: WalkMode.visitAll, + cancel: cancel.token + }); + + if (!sceneVar) { + return; + } + + //now look for `${sceneVar}.show()` + entryFunc.findChild((node) => { + if ( + isCallExpression(node) && + isDottedGetExpression(node.callee) && + node.callee.name?.text?.toLowerCase() === 'show' && + node.callee. + ) { + return; + } + }); + + if (entryFunc) { + const finder = lineColumn(entryPoint.fileContents); + const range = entryFunc.func.body.range; + const startIndex = finder.toIndex(range.start.line, range.start.character); + const stopIndex = finder.toIndex(range.end.line, range.end.character); + const functionBody = entryPoint.fileContents.substring(startIndex, stopIndex); + + const [, sceneVariable] = /([a-z0-9_\[\]"]+)\s*=\s*createObject\s*\(\s*"roSGScreen"\s*)/i.exec(functionBody); + if (sceneVariable) { + const regexp = new RegExp(`\\b${sceneVariable}\\s*\\.\\s*show\\(.*\\)`, 'i'); + const match = regexp.exec(functionBody); + const startPosition = finder.fromIndex(startIndex + match.index); + const stopPosition = finder.fromIndex(startIndex + match.index + match[0].length); + return { + range: bscUtil.createRange( + startPosition.line, + startPosition.col, + stopPosition.line, + stopPosition.col + ) + }; + } + } + } + public static RDB_ODC_NODE_CODE = `if true = CreateObject("roAppInfo").IsDev() then m.vscode_rdb_odc_node = createObject("roSGNode", "RTA_OnDeviceComponent") ' RDB OnDeviceComponent`; public static RDB_ODC_ENTRY = 'vscode_rdb_on_device_component_entry'; /** @@ -427,10 +516,10 @@ export class Project { return; } try { - let files = await globAsync(`${this.rdbFilesBasePath}/**/*`, { + let files = await fastGlob(`${this.rdbFilesBasePath}/**/*`, { cwd: './', absolute: false, - follow: true + followSymbolicLinks: true }); for (let filePathAbsolute of files) { const promises = [];