diff --git a/CHANGELOG.md b/CHANGELOG.md index a38a63e6..19fa0e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [1.20.0] - 2019-10-01 +### Added + - "port is in use" crash message when serving component libraries +### Changed + - The Roku stacktrace includes all function names back as fully lower case. The extension reads the original files and attempts to find the correct case for every function. These results were not being cached, but are now cached in order to improve performance. +### Fixed + - some syntax colors related to object function calls + + + ## [1.19.6] - 2019-09-23 ### Fixed - bugs in language grammar (syntax highlighting) @@ -351,6 +363,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +[1.20.0]: https://github.com/RokuCommunity/vscode-brightscript-language/compare/v1.19.6...v1.20.0 [1.19.6]: https://github.com/RokuCommunity/vscode-brightscript-language/compare/v1.19.5...v1.19.6 [1.19.5]: https://github.com/RokuCommunity/vscode-brightscript-language/compare/v1.19.4...v1.19.5 [1.19.4]: https://github.com/RokuCommunity/vscode-brightscript-language/compare/v1.19.3...v1.19.4 diff --git a/package-lock.json b/package-lock.json index 632e7d63..be163b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "brightscript", - "version": "1.19.6", + "version": "1.20.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3257,6 +3257,14 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-port": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz", + "integrity": "sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==", + "requires": { + "type-fest": "^0.3.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -6838,6 +6846,11 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + }, "typed-rest-client": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", diff --git a/package.json b/package.json index f29a94cc..3ed5452b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "brightscript", "displayName": "BrightScript Language", - "version": "1.19.6", + "version": "1.20.0", "publisher": "celsoaf", "description": "Language support for Roku's BrightScript language.", "author": { @@ -49,6 +49,7 @@ "fast-xml-parser": "^3.12.16", "find-in-files": "^0.5.0", "fs-extra": "^7.0.1", + "get-port": "^5.0.0", "glob": "^7.1.3", "hoek": "^6.1.2", "iconv-lite": "0.4.24", diff --git a/src/BrightScriptDebugSession.ts b/src/BrightScriptDebugSession.ts index 7d2def3b..247a0c98 100644 --- a/src/BrightScriptDebugSession.ts +++ b/src/BrightScriptDebugSession.ts @@ -664,6 +664,38 @@ export class BrightScriptDebugSession extends DebugSession { this.sendResponse(response); } + /** + * The stacktrace sent by Roku forces all BrightScript function names to lower case. + * This function will scan the source file, and attempt to find the exact casing from the function definition. + * Also, this function caches results, so it should be drastically faster than the previous implementation + * that would read the source file every time + */ + private async getCorrectFunctionNameCase(sourceFilePath: string, functionName: string) { + let lowerSourceFilePath = sourceFilePath.toLowerCase(); + let lowerFunctionName = functionName.toLowerCase(); + //create the lookup if it doesn't exist + if (!this.functionNameCaseLookup[lowerSourceFilePath]) { + this.functionNameCaseLookup[lowerSourceFilePath] = {}; + + let fileContents = (await fsExtra.readFile(sourceFilePath)).toString(); + //read the file contents + let regexp = /^\s*(?:sub|function)\s+([a-z0-9_]+)/gim; + let match: RegExpMatchArray; + + //create a cache of all function names in this file + while (match = regexp.exec(fileContents)) { + let correctFunctionName = match[1]; + this.functionNameCaseLookup[lowerSourceFilePath][correctFunctionName.toLowerCase()] = correctFunctionName; + } + } + return this.functionNameCaseLookup[lowerSourceFilePath][lowerFunctionName]; + } + private functionNameCaseLookup = {} as { + [lowerSourceFilePath: string]: { + [lowerFunctionName: string]: string + } + }; + protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { this.log('stackTraceRequest'); let frames = []; @@ -672,18 +704,17 @@ export class BrightScriptDebugSession extends DebugSession { let stackTrace = await this.rokuAdapter.getStackTrace(); for (let debugFrame of stackTrace) { - let clientPath = this.convertDebuggerPathToClient(debugFrame.filePath); let clientLineNumber = this.convertDebuggerLineToClientLine(debugFrame.filePath, debugFrame.lineNumber); //the stacktrace returns function identifiers in all lower case. Try to get the actual case //load the contents of the file and get the correct casing for the function identifier try { - let fileContents = (await fsExtra.readFile(clientPath)).toString(); - let match = new RegExp(`(?:sub|function)\\s+(${debugFrame.functionIdentifier})`, 'i').exec(fileContents); - if (match) { - debugFrame.functionIdentifier = match[1]; + let functionName = await this.getCorrectFunctionNameCase(clientPath, debugFrame.functionIdentifier); + if (functionName) { + debugFrame.functionIdentifier = functionName; } } catch (e) { + console.error(e); } let frame = new StackFrame( diff --git a/src/ComponentLibraryServer.ts b/src/ComponentLibraryServer.ts index 2f78d2e5..0d8cf4c0 100644 --- a/src/ComponentLibraryServer.ts +++ b/src/ComponentLibraryServer.ts @@ -4,12 +4,19 @@ import * as os from 'os'; import * as path from 'path'; import * as url from 'url'; +import { util } from './util'; + export class ComponentLibraryServer { public componentLibrariesOutDir: string; public startStaticFileHosting(componentLibrariesOutDir: string, port: number, sendDebugLogLine) { + // Make sure the requested port is not already being used by another service + if (util.isPortInUse(port)) { + throw new Error(`Could not host component library files.\nPort ${port} is currently occupied.`); + } + this.componentLibrariesOutDir = componentLibrariesOutDir; // #region prepare static file hosting diff --git a/src/util.spec.ts b/src/util.spec.ts index 5dcca1b0..e92feec2 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -2,6 +2,8 @@ import * as assert from 'assert'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; +import * as getPort from 'get-port'; +import * as net from 'net'; import * as path from 'path'; import * as sinonActual from 'sinon'; @@ -135,6 +137,32 @@ describe('Util', () => { }); }); + describe('isPortInUse', () => { + let otherServer: net.Server; + let port: number; + + beforeEach(async () => { + port = await getPort(); + otherServer = await new Promise((resolve, reject) => { + const tester = net.createServer() + .once('listening', () => resolve(tester)) + .listen(port); + }); + }); + + it('should detect when a port is in use', async () => { + assert.equal(true, await util.isPortInUse(port)); + }); + + it('should detect when a port is not in use', async () => { + assert.equal(false, await util.isPortInUse(port + 1)); + }); + + afterEach(async () => { + await otherServer.close(); + }); + }); + describe('objectDiff', () => { let objectA; let objectB; diff --git a/src/util.ts b/src/util.ts index 79173aa6..1e0d42e6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as fsExtra from 'fs-extra'; +import * as net from 'net'; import * as path from 'path'; const extensions = ['.js', '.ts', '.json', '.jsx', '.tsx', '.vue', '.css', '.mcss', '.scss', '.less', '.html']; @@ -115,6 +116,19 @@ class Util { } } + /** + * Checks to see if the port is already in use + * @param port target port to check + */ + public async isPortInUse(port: number): Promise { + return new Promise((resolve, reject) => { + const tester = net.createServer() + .once('error', (err: any) => (err.code === 'EADDRINUSE' ? resolve(true) : reject(err))) + .once('listening', () => tester.once('close', () => resolve(false)).close()) + .listen(port); + }); + } + /** * With return the differences in two objects * @param obj1 base target diff --git a/syntaxes/brightscript.tmLanguage.json b/syntaxes/brightscript.tmLanguage.json index 3fc149f5..3147b722 100644 --- a/syntaxes/brightscript.tmLanguage.json +++ b/syntaxes/brightscript.tmLanguage.json @@ -109,12 +109,9 @@ ] }, "object_properties": { - "match": "(?i:\\b([a-z_][a-z0-9_]*)\\.([a-z_][a-z0-9_]*))", + "match": "(?i:\\b\\.((?:[a-z0-9_])*)(?!\\s*\\()\\b)", "captures": { "1": { - "name": "entity.name.variable.local.brs" - }, - "2": { "name": "variable.other.object.property.brs" } } @@ -191,7 +188,7 @@ "name": "entity.name.variable.local.brs" } }, - "match": "(?i:\\b([a-z_][a-z0-9_\\$%!#]*)\\b)" + "match": "(?i:\\b(?