diff --git a/CHANGELOG.md b/CHANGELOG.md index f64a201..09542ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,4 +41,14 @@ - Fixed bug that introduced spaces to blank lines - Introduced webpack to bundle the extension - Removed docs from the bundled extension (they can still be accessed online at the repo) -- Improvements to RestFS API \ No newline at end of file +- Improvements to RestFS API + +## 2.0.7 + +- Fixed a bug with the "customWordPath" option (#24) +- Fixed auto-formatting bug (#45) +- Added documentation for the RestFS API (#89) +- Fixed bug with space-prefixed labels (#96) +- Improved linting (#98) +- Improved label detection with GOTO/GOSUB (#99) +- Fixed bug with FOR/NEXT linting with labels (#104) \ No newline at end of file diff --git a/Syntaxes/QM.tmLanguage.json b/Syntaxes/QM.tmLanguage.json index 50de223..77ac507 100644 --- a/Syntaxes/QM.tmLanguage.json +++ b/Syntaxes/QM.tmLanguage.json @@ -3,11 +3,11 @@ "name": "QM# BASIC", "patterns": [ { - "match": "(\"([^\"]|\"\")*\")|('([^']|'')*')|(\\\\([^\\\\]|\\\\\\\\)*\\\\)", + "match": "'.*?'|\".*?\"|\\\\.*?\\\\", "name": "string.other.quoted-or-unquoted.mvon" }, { - "match": "(^[0-9]+\\s)|(^[0-9]+:\\s)|(^[\\w]+:(?!=))|(^[\\w\\.\\w]+:(?!=))", + "match": "^\\s*([\\w.]+:(?!=)|[0-9.]+)", "name": "string.other.quoted-or-unquoted.mvon" }, { diff --git a/Syntaxes/UniVerse.tmLanguage.json b/Syntaxes/UniVerse.tmLanguage.json index a01b8c3..077696f 100644 --- a/Syntaxes/UniVerse.tmLanguage.json +++ b/Syntaxes/UniVerse.tmLanguage.json @@ -3,11 +3,11 @@ "name": "MVON# BASIC", "patterns": [ { - "match": "(\"([^\"]|\"\")*\")|('([^']|'')*')|(\\\\([^\\\\]|\\\\\\\\)*\\\\)", + "match": "'.*?'|\".*?\"|\\\\.*?\\\\", "name": "string.other.quoted-or-unquoted.mvon" }, { - "match": "(^[0-9]+\\s)|(^[0-9]+:\\s)|(^[\\w]+:(?!=))|(^[\\w\\.\\w]+:(?!=))", + "match": "^\\s*([\\w.]+:(?!=)|[0-9.]+)", "name": "string.other.quoted-or-unquoted.mvon" }, { diff --git a/Syntaxes/jBASE.tmLanguage.json b/Syntaxes/jBASE.tmLanguage.json index f24e8be..8def363 100644 --- a/Syntaxes/jBASE.tmLanguage.json +++ b/Syntaxes/jBASE.tmLanguage.json @@ -3,11 +3,11 @@ "name": "jBASE PickBASIC", "patterns": [ { - "match": "(\"([^\"]|\"\")*\")|('([^']|'')*')|(\\\\([^\\\\]|\\\\\\\\)*\\\\)", + "match": "'.*?'|\".*?\"|\\\\.*?\\\\", "name": "string.other.quoted-or-unquoted.jbase" }, { - "match": "(^[0-9]+\\s)|(^[0-9]+:\\s)|(^[\\w]+:(?!=))|(^[\\w\\.\\w]+:(?!=))", + "match": "^\\s*([\\w.]+:(?!=)|[0-9.]+)", "name": "string.other.quoted-or-unquoted.jbase" }, { diff --git a/Syntaxes/mvon.tmLanguage.json b/Syntaxes/mvon.tmLanguage.json index 0252259..d039f61 100644 --- a/Syntaxes/mvon.tmLanguage.json +++ b/Syntaxes/mvon.tmLanguage.json @@ -3,11 +3,11 @@ "name": "MVON# BASIC", "patterns": [ { - "match": "(\"([^\"]|\"\")*\")|('([^']|'')*')|(\\\\([^\\\\]|\\\\\\\\)*\\\\)", + "match": "'.*?'|\".*?\"|\\\\.*?\\\\", "name": "string.other.quoted-or-unquoted.mvon" }, { - "match": "(^[0-9]+)|(^[0-9]+:\\s)|(^[\\w]+:(?!=))|(^[\\w\\.\\w]+:(?!=))", + "match": "^\\s*([\\w.]+:(?!=)|[0-9.]+)", "name": "string.other.quoted-or-unquoted.mvon" }, { diff --git a/client/package.json b/client/package.json index 2f00502..78ca538 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,7 @@ "name": "mvbasic", "displayName": "MV Basic", "description": "MV Basic", - "version": "2.0.6", + "version": "2.0.7", "publisher": "mvextensions", "license": "MIT", "icon": "../images/mvbasic-logo.png", @@ -38,4 +38,4 @@ "@types/node": "^13.13.4", "@types/vscode": "^1.44.0" } -} +} \ No newline at end of file diff --git a/client/src/extension.ts b/client/src/extension.ts index 9bd9336..14ec135 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -125,10 +125,13 @@ export function activate(context: vscode.ExtensionContext) { var lines = contents.replace('\r', '').split('\n'); for (let i = 0; i < lines.length; i++) { let parts = lines[i].split(':') - customWordDict.set(parts[0].replace("\"", "").replace("\"", ""), parts[1].replace("\"", "").replace("\"", "")) - customWordlist += parts[0].replace('"', '').replace("\"", "") + "|"; + if (parts.length >= 2) { + customWordDict.set(parts[0].replace("\"", "").replace("\"", ""), parts[1].replace("\"", "").replace("\"", "")) + customWordlist += parts[0].replace("\"", '').replace("\"", "") + "|"; + } } - customWordlist = customWordlist.substr(0, customWordlist.length - 1) + ")"; + if (customWordlist.length > 1) + customWordlist = customWordlist.substr(0, customWordlist.length - 1) + ")"; } @@ -270,25 +273,24 @@ export function activate(context: vscode.ExtensionContext) { var edits: vscode.TextEdit[] = [] if (formattingEnabled) { - let rBlockStart = new RegExp("^(lock |key\\(|if |commit |rollback |readnext |open |write |writeu |writev |writevu |read |readv |readu |readt |readvu |matreadu |locate |locate\\(|openseq |matread |create |readlist |openpath |find |findstr |bscan)", "i") - let rBlockCase = new RegExp("(^begin case)", "i") - let rBlockTransaction = new RegExp("(^begin transaction|^begin work)", "i") - let rBlockEndCase = new RegExp("(^end case)", "i") - let rBlockEndTransaction = new RegExp("(^end transaction|^end work)", "i") - let rBlockAlways = new RegExp("^(for |loop$|loop\\s+)", "i") - let rBlockContinue = new RegExp("(then$|else$|case$|on error$|locked$)", "i") - let rBlockEnd = new RegExp("^(end|repeat|next\\s.+)$", "i") - let rElseEnd = new RegExp("^(end else\\s.+)", "i") - let rLabel = new RegExp("(^[0-9]+\\s)|(^[0-9]+:\\s)|(^[\\w]+:)"); + let rBlockStart = new RegExp("^(begin case$|lock |key\\(|if |commit |rollback |readnext |open |write |writeu |writev |writevu |read |readv |readu |readt |readvu |matreadu |locate |locate\\(|openseq |matread |create |readlist |openpath |find |findstr |bscan)", "i") + let rBlockAlways = new RegExp("^(for |loop( |$))", "i") + let rBlockContinue = new RegExp(" (then|else|case|on error|locked)$", "i") + let rBlockEnd = new RegExp("^(end|end case|next|next\\s+.+|repeat)$| repeat$", "i") + let rBlockCase = new RegExp("^begin case$", "i") + let rBlockEndCase = new RegExp("^end case$", "i") + let rBlockTransaction = new RegExp("^(begin transaction|begin work)", "i") + let rBlockEndTransaction = new RegExp("^(end transaction|end work)", "i") + let rElseEnd = new RegExp("^end else\\s+?.+?", "i") + let rLabel = new RegExp("^([\\w\\.]+:(?!=)|[0-9\\.]+)"); let rComment = new RegExp("^\\s*(\\*|!|REM\\s+?).*", "i") let tComment = new RegExp(";\\s*(\\*|!|REM\\s+?).*", "i"); - let lComment = new RegExp("(^[0-9]+\\s+\\*)|(^[0-9]+\\s+;)|(^[0-9]+\\*)|(^[0-9]+;)") // number label with comments after - let trailingComment = new RegExp("(\\*.+)|(;+)") - let spaces = " " + let lComment = new RegExp("^\\s*([\\w\\.]+:(?!=)|[0-9\\.]+)(\\s*(\\*|!|REM\\s+?).*)", "i") // a label with comments after + let qStrings = new RegExp("'.*?'|\".*?\"|\\\\.*?\\\\", "g"); + let rParenthesis = new RegExp("\\(.*\\)", "g"); if (indent === undefined) { indent = 3 } if (margin === undefined) { margin = 5 } - // first build a list of labels in the program and indentation levels let Level = 0 var RowLevel: number[] = [] @@ -296,58 +298,71 @@ export function activate(context: vscode.ExtensionContext) { let curLine = document.lineAt(i); let line = curLine.text; - if (rComment.test(line.trim()) == true) { continue } - // TODO ignore comment lines and - if (line.trim().startsWith("$")) { continue } - // remove trailing comments - if (tComment.test(line.trim()) == true) { - let comment = tComment.exec(line.trim()); - line = line.trim().replace(comment[0], ""); + // replace comment line with blank line + line = line.replace(rComment, ""); + // remove comments after label (no semi-colon) + if (lComment.test(line)) { + let comment = lComment.exec(line); + line = comment![1]; } - lComment.lastIndex = 0; - if (lComment.test(line.trim()) === true) { - let comment = trailingComment.exec(line.trim()); - if (comment != null) { - line = line.trim().replace(comment[0], ""); - } + + // remove trailing comments with a semi-colon + line = line.replace(tComment, ""); + + // replace contents of parenthesis with spaces, maintaining original + // character positions for intellisense error highlighting. + let v = rParenthesis.exec(line); + if (v !== null) { + let value = "(" + " ".repeat(v[0].length - 2) + ")"; + line = line.replace(rParenthesis, value); } + + // replace contents of quoted strings with spaces, maintaining original + // character positions for intellisense error highlighting. + v = qStrings.exec(line); + if (v !== null) { + let value = "'" + " ".repeat(v[0].length - 2) + "'"; + line = line.replace(qStrings, value); + } + + // Trim() leading & trailing spaces + line = line.trim(); + // check opening and closing block for types // check block statements var position = i RowLevel[i] = Level - if (rBlockStart.test(line.trim()) == true) { + if (rBlockStart.test(line)) { Level++ - if (rBlockContinue.test(line.trim()) == false) { + if (!rBlockContinue.test(line)) { // single line statement Level-- } position = i + 1 } - if (rBlockCase.test(line.trim()) == true) { - // increment 2 to cater for case statement - Level++ + if (rBlockCase.test(line)) { + // increment to cater for case statement Level++ position = i + 1 } - if (rBlockEndCase.test(line.trim()) == true) { - // decrement 2 to cater for case statement - Level-- + if (rBlockEndCase.test(line)) { + // decrement to cater for case statement Level-- } - if (rElseEnd.test(line.trim()) == true) { + if (rElseEnd.test(line)) { // decrement 1 to cater for end else stements Level-- } - if (rBlockTransaction.test(line.trim()) == true) { - // increment 2 to cater for case statement + if (rBlockTransaction.test(line)) { + // increment for transaction statement Level++ position = i + 1 } - if (rBlockEndTransaction.test(line.trim()) == true) { - // decrement 2 to cater for case statement + if (rBlockEndTransaction.test(line)) { + // decrement for transaction statement Level-- } if (rBlockAlways.test(line.trim())) { @@ -360,40 +375,45 @@ export function activate(context: vscode.ExtensionContext) { } RowLevel[position] = Level } + + // Output formatted lines for (var i = 0; i < document.lineCount; i++) { const line = document.lineAt(i); + let lineText = line.text.trim(); + // ignore labels - if (rLabel.test(line.text.trim()) == true) { continue } + if (rLabel.test(lineText)) { continue } var indentation = 0 if (RowLevel[i] === undefined) { continue; } indentation = (RowLevel[i] * indent) + margin - if (new RegExp("(^case\\s)", "i").test(line.text.trim()) == true) { + if (new RegExp("(^case\\s)", "i").test(lineText)) { indentation -= indent } - if (new RegExp("(^while\\s|^until\\s)", "i").test(line.text.trim()) == true) { + if (new RegExp("(^while\\s|^until\\s)", "i").test(lineText)) { indentation -= indent } - if (new RegExp("(^end else$)", "i").test(line.text.trim()) == true) { + + // remove trailing comments with a semi-colon + let tempLine = lineText.replace(tComment, "").trim(); + if (new RegExp("(^end else$)", "i").test(tempLine)) { indentation -= indent } - var blankLine = line.text.replace(/\s/g, "") + var blankLine = lineText.replace(/\s/g, "") if (indentation < 1 || blankLine.length == 0) { - edits.push(vscode.TextEdit.replace(line.range, line.text.trim())) + edits.push(vscode.TextEdit.replace(line.range, lineText)) } else { - var regEx = "\\s{" + indentation + "}" - var formattedLine = new RegExp(regEx).exec(spaces)[0] + line.text.trim() + var formattedLine = " ".repeat(indentation) + lineText var formatted = vscode.TextEdit.replace(line.range, formattedLine) edits.push(formatted) } } } - return edits } }); @@ -451,11 +471,11 @@ export function activate(context: vscode.ExtensionContext) { } activeEditor.setDecorations(customDecoration, customWords); } - + let api = { getRestFS(): RestFS { return RESTFS; - } + } }; return api; } diff --git a/doc/DeveloperIntro.md b/doc/DeveloperIntro.md index 5e30276..ccc08d7 100644 --- a/doc/DeveloperIntro.md +++ b/doc/DeveloperIntro.md @@ -180,7 +180,7 @@ An introduction to how syntax highlights work in VSCode: https://code.visualstud A description of the various "scopes" typically used: https://macromates.com/manual/en/language_grammars#naming_conventions -Using "Developer: Inspect TM Scopes" to see how a particular token is interpreted. This will show both the source as well as the scope for any language elements selected. +Using "Developer: Inspect Editor Token and Scopes" to see how a particular token is interpreted. This will show both the source as well as the scope for any language elements selected. ![Dev Inspect Scopes](screenshots/devguide/dev_inspect_scopes.gif) diff --git a/package.json b/package.json index 4bbbbb3..f60556f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mvbasic", "displayName": "MV Basic", "description": "MV Basic", - "version": "2.0.6", + "version": "2.0.7", "publisher": "mvextensions", "license": "MIT", "icon": "images/mvbasic-logo.png", @@ -341,4 +341,4 @@ "webpack-cli": "^3.3.11" }, "dependencies": {} -} +} \ No newline at end of file diff --git a/server/package.json b/server/package.json index 5abee1a..2a0ed09 100644 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "mvbasic", "displayName": "MV Basic", "description": "MV Basic", - "version": "2.0.6", + "version": "2.0.7", "publisher": "mvextensions", "license": "MIT", "icon": "../images/mvbasic-logo.png", @@ -36,4 +36,4 @@ "devDependencies": { "@types/node": "^13.13.4" } -} +} \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index 40b2e34..9a81101 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -43,7 +43,6 @@ let connection: IConnection = createConnection( let useCamelcase = true; let ignoreGotoScope = false; -let shouldSendDiagnosticRelatedInformation: boolean | undefined = false; // The settings interface describe the server relevant settings part interface Settings { @@ -130,20 +129,18 @@ function convertRange( } function getWord(line: string, wordCount: number): string { - let xpos = 0; - let startpos = 0; - while (xpos < line.length) { - if (line.substr(xpos, 1) == " " || line.substr(xpos, 1) == "=") { - wordCount--; - if (wordCount == 0) { - return line.substr(startpos, xpos - startpos); - } - startpos = xpos + 1; - } + if (line.length == 0 || wordCount < 1) { + return line; + } + let rWhitespace = new RegExp("\\s{2,}"); // 2 or more whitespace chars + line = line.replace(rWhitespace, " ").trim(); + let words = line.split(" "); - xpos++; + if (wordCount > words.length) { + return ""; } - return line.substr(startpos, xpos - startpos); + + return words[wordCount - 1]; } function loadIntelliSense() { @@ -233,152 +230,156 @@ function loadIntelliSense() { function validateTextDocument(textDocument: TextDocument): void { let diagnostics: Diagnostic[] = []; - let originalLines = textDocument.getText().split(/\r?\n/g); let lines: DocumentLine[] = []; originalLines.forEach((lineOfCode, index) => lines.push({ lineNumber: index, lineOfCode })); let problems = 0; LabelList.length = 0; - // regex to extract labels - let rLabel = new RegExp( - "(^[0-9]+\\b)|(^[0-9]+)|(^[0-9]+:\\s)|(^[\\w\\.]+:(?!\\=))", - "i" - ); - // regex for statements that start a block - let rBlockStart = new RegExp( - "(^if |begin case|^readnext |open |read |readv |readu |readt |locate |openseq |matread |create |readlist |openpath |find |findstr )", - "i" - ); - let rBlockAlways = new RegExp("^(for |loop$|loop\\s+)", "i"); - let rBlockContinue = new RegExp("(then$|else$|case$|on error$|locked$)", "i"); - let rBlockEnd = new RegExp("^(end|end case|repeat|.+repeat$|next\\s.+)$", "i"); - let rStartFor = new RegExp("^(for )", "i"); - let rStartLoop = new RegExp("^(loop$|loop\\s+)", "i"); - let rStartCase = new RegExp("(^begin case)", "i"); - let rEndFor = new RegExp("(^next\\s)", "i"); - let rEndLoop = new RegExp("(repeat$)", "i"); - let rEndCase = new RegExp("(^end case)", "i"); - let rElseEnd = new RegExp("^(end else\\s.+)", "i"); + + let rBlockStart = new RegExp("(^| )(begin case$|(if|readnext|open|read|readv|readu|readt|locate|openseq|matread|create|readlist|openpath|find|findstr)\\s+?)", "i"); + let rBlockAlways = new RegExp("(^| )(for |loop( |$))", "i"); + let rBlockContinue = new RegExp(" (then|else|case|on error|locked)$", "i"); + let rBlockEnd = new RegExp("(^| )(end|end case|next|next\\s+.+|repeat)$", "i"); + let rStartFor = new RegExp("(^| )for\\s+.+", "i"); + let rEndFor = new RegExp("(^| )next($|\\s+.+$)", "i"); + let rStartLoop = new RegExp("(^| )loop\\s*?", "i"); + let rEndLoop = new RegExp("(^| )repeat\\s*$", "i"); + let rStartCase = new RegExp("(^| )begin case$", "i"); + let rEndCase = new RegExp("^\\s*end case$", "i"); + let rElseEnd = new RegExp("^\\s*end else$", "i"); + let rLabel = new RegExp("^\\s*([\\w\\.]+:(?!=)|[0-9\\.]+)", "i"); let rComment = new RegExp("^\\s*(\\*|!|REM\\s+?).*", "i"); // Start-of-line 0-or-more whitespace {* ! REM} Anything let tComment = new RegExp(";\\s*(\\*|!|REM\\s+?).*", "i"); // (something); {0-or-more whitespace} {* ! REM} Anything - let lComment = new RegExp("(^\\s*[0-9]+)(\\s*\\*.*)"); // number label with comments after - let trailingComment = new RegExp("(\\*.+)|(;+)"); - let qStrings = new RegExp( - "(\"([^\"]|\"\")*\")|('([^']|'')*')|(\\\\([^\\\\]|\\\\\\\\)*\\\\)", - "g" - ); + let lComment = new RegExp("^\\s*([\\w\\.]+:(?!=)|[0-9\\.]+)(\\s*(\\*|!|REM\\s+?).*)", "i"); + let qStrings = new RegExp("'.*?'|\".*?\"|\\\\.*?\\\\", "g"); + let rParenthesis = new RegExp("\\(.*\\)", "g"); let noCase = 0; let noLoop = 0; let noEndLoop = 0; let noEndCase = 0; - let forDict = new Map(); + // first build a list of labels in the program and indentation levels, strip comments, break up ; delimited lines let Level = 0; var RowLevel: number[] = [lines.length]; + let forNext = [] // FOR statements list + let forNextErr = [] // FOR NEXT errors list + for (var i = 0; i < lines.length && problems < maxNumberOfProblems; i++) { let line = lines[i]; - // ignore all comment lines - if (rComment.test(line.lineOfCode.trim()) === true) { - continue; + + // Begin cleanup lines[] array -- Remove and replace irrelevant code. + + // replace comment line with blank line + line.lineOfCode = line.lineOfCode.replace(rComment, ""); + + // remove comments after label (no semi-colon) + if (lComment.test(line.lineOfCode)) { + let comment = lComment.exec(line.lineOfCode); + line.lineOfCode = comment![1]; } + // remove trailing comments with a semi-colon - if (tComment.test(line.lineOfCode.trim()) === true) { - line.lineOfCode = line.lineOfCode.replace(tComment, "").trim(); + line.lineOfCode = line.lineOfCode.replace(tComment, ""); + + // replace contents of parenthesis with spaces, maintaining original + // character positions for intellisense error highlighting. + let v = rParenthesis.exec(line.lineOfCode); + if (v !== null) { + let value = "(" + " ".repeat(v[0].length - 2) + ")"; + line.lineOfCode = line.lineOfCode.replace(rParenthesis, value); } - // remove comments after label (no semi-colon) - if (lComment.test(line.lineOfCode.trim()) === true) { - let comment = lComment.exec(line.lineOfCode.trim()); // This does the regex match again, but assigns the results to comment array - line.lineOfCode = comment![1]; + // replace contents of quoted strings with spaces, maintaining original + // character positions for intellisense error highlighting. + v = qStrings.exec(line.lineOfCode); + if (v !== null) { + let value = "'" + " ".repeat(v[0].length - 2) + "'"; + line.lineOfCode = line.lineOfCode.replace(qStrings, value); } - /* Before we do anything else, split line on ; *except* inside strings or parens. - Ug! One problem with this approach is it throws off the line number in the error report... - This helps deal with lines like: FOR F=1 TO 20;CRT "NEW;":REPLACE(REC,1,0,0;'XX');NEXT F ;* COMMENT - There may be a way to do this with regexp, but it gets super hairy. - See: https://stackoverflow.com/questions/23589174/regex-pattern-to-match-excluding-when-except-between - */ + // Trim() trailing spaces + line.lineOfCode = line.lineOfCode.trimRight(); + + // Save cleaned line + lines[i] = line; + + // End cleanup of lines[] array + + /* Before we do anything else, split line into statements on semicolon + Should split lines like: + FOR F=1 TO 20;CRT "NEW=":F;NEXT F ;* COMMENT + Should not split lines such as: + LEASE.TYPE=OCONV(ID,"TLS.MASTER,LS.BILLING;X;38;38") + locate(acontinent,continents,1;position;’al’) then crt acontinent:’ is already there’ + */ if (line.lineOfCode.indexOf(";") > 0) { - let inString = false; - for (var j = 0; j < line.lineOfCode.length; j++) { - let ch = line.lineOfCode.charAt(j); - if ( - ch === '"' || - ch === "'" || - ch === "\\" || - ch === "(" || - ch === ")" - ) { - inString = !inString; - } - if (ch === ";" && !inString) { - let left = line.lineOfCode.slice(0, j); - let right = line.lineOfCode.slice(j + 1); - // Push the right side into the array lines, and deal with it later (including more splitting) - lines[i] = { lineNumber: line.lineNumber, lineOfCode: left }; - lines.splice(i + 1, 0, { lineNumber: line.lineNumber, lineOfCode: right }); - line = lines[i]; - break; - } + let a = line.lineOfCode.split(";"); + // Replace line i with the first statement + lines[i] = { lineNumber: line.lineNumber, lineOfCode: a[0].trimRight() }; + line = lines[i]; + // Insert new lines for each subsequent statement, but keep line.lineNumber the same + for (let j = 1; j < a.length; j++) { + lines.splice(i + j, 0, { lineNumber: line.lineNumber, lineOfCode: a[j].trimRight() }); } } - // check opening and closing block FOR/NEXT - if (rStartFor.test(line.lineOfCode.trim())) { - let forvar = getWord(line.lineOfCode.trim(), 2); - let o = forDict.get(forvar); - if (typeof o == "undefined") { - o = { ctr: 1, line: i }; - } else { - o = { ctr: o.ctr + 1, line: i }; - } - forDict.set(forvar, o); + // check opening and closing block FOR/NEXT - Track matches + // and build errors list (forNextErr[]). + let arrFor = rStartFor.exec(line.lineOfCode) + if (arrFor !== null) { + let forvar = getWord(arrFor[0], 2); + forNext.push({ forVar: forvar, forLine: i }); } - if (rEndFor.test(line.lineOfCode.trim())) { - let nextvar = getWord(line.lineOfCode.trim(), 2); - let o = forDict.get(nextvar); - if (typeof o == "undefined") { - o = { ctr: -1, line: i }; + let arrNext = rEndFor.exec(line.lineOfCode) + if (arrNext !== null) { + let nextvar = getWord(arrNext[0], 2); + let pos = forNext.length - 1; + if (pos < 0) { + forNextErr.push({ errMsg: "Missing FOR statement - NEXT " + nextvar, errLine: i }); } else { - o = { ctr: o.ctr - 1, line: i }; + let o = forNext[pos]; + if (nextvar != "" && o.forVar !== nextvar) { + forNextErr.push({ errMsg: "Missing NEXT statement - FOR " + o.forVar, errLine: o.forLine }); + forNextErr.push({ errMsg: "Missing FOR statement - NEXT " + nextvar, errLine: i }); + } + forNext.pop(); } - forDict.set(nextvar, o); } // Check for CASE/LOOP - if (rStartCase.test(line.lineOfCode.trim()) == true) { + if (rStartCase.test(line.lineOfCode)) { noCase++; } - if (rEndCase.test(line.lineOfCode.trim()) == true) { + if (rEndCase.test(line.lineOfCode)) { noEndCase++; } - if (rStartLoop.test(line.lineOfCode.trim()) == true) { + if (rStartLoop.test(line.lineOfCode)) { noLoop++; } - if (rEndLoop.test(line.lineOfCode.trim()) == true) { + if (rEndLoop.test(line.lineOfCode)) { noEndLoop++; } // check block statements - if (rBlockStart.test(line.lineOfCode.trim()) == true) { + if (rBlockStart.test(line.lineOfCode)) { Level++; - if (rBlockContinue.test(line.lineOfCode.trim()) == false) { + if (rBlockContinue.test(line.lineOfCode) === false) { // single line statement Level--; } } - if (rBlockAlways.test(line.lineOfCode.trim())) { + if (rBlockAlways.test(line.lineOfCode)) { Level++; } - if (rBlockEnd.test(line.lineOfCode.trim())) { + if (rBlockEnd.test(line.lineOfCode)) { Level--; } - if (rElseEnd.test(line.lineOfCode.trim()) == true) { + if (rElseEnd.test(line.lineOfCode)) { // decrement 1 to cater for end else stements Level--; } // 10 10: start: labels - if (rLabel.test(line.lineOfCode.trim()) === true) { + if (rLabel.test(line.lineOfCode)) { let label = ""; if (line !== null) { let labels = rLabel.exec(line.lineOfCode.trim()); @@ -391,50 +392,32 @@ function validateTextDocument(textDocument: TextDocument): void { RowLevel[i] = Level; } - // Missing FOR/NEXT statements - for (let forvar of forDict.keys()) { - let o = forDict.get(forvar); - let errorMsg = ""; - if (o.ctr != 0) { - if (o.ctr > 0) { - errorMsg = "Missing NEXT Statement - FOR " + forvar; - } else { - errorMsg = "Missing FOR Statement - NEXT " + forvar; - } - let line = lines[o.line]; - let diagnosic: Diagnostic = { - severity: DiagnosticSeverity.Error, - range: { - start: { line: line.lineNumber, character: 0 }, - end: { line: line.lineNumber, character: line.lineOfCode.length } - }, - message: errorMsg, - source: "MV Basic" - }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: line.lineNumber, character: 0 }, - end: { line: line.lineNumber, character: line.lineOfCode.length } - } - }, - message: errorMsg - } - ]; - } - diagnostics.push(diagnosic); + // Missing NEXT errors. More FOR statements than NEXT values matched. + forNext.forEach(function (o) { + forNextErr.push({ errMsg: "Missing NEXT statement - FOR " + o.forVar, errLine: o.forLine }); + }); + + forNextErr.forEach(function (o) { + let errorMsg = o.errMsg; + let line = lines[o.errLine]; + let diagnosic: Diagnostic = { + severity: DiagnosticSeverity.Error, + range: { + start: { line: line.lineNumber, character: 0 }, + end: { line: line.lineNumber, character: line.lineOfCode.length } + }, + message: errorMsg, + source: "MV Basic" } - } + diagnostics.push(diagnosic); + }); // Missing END CASE statement if (noCase != noEndCase) { // find the innermost for for (var i = 0; i < lines.length && problems < maxNumberOfProblems; i++) { let line = lines[i]; - if (rStartCase.test(line.lineOfCode.trim()) == true) { + if (rStartCase.test(line.lineOfCode)) { noCase--; } if (noCase == 0) { @@ -447,20 +430,6 @@ function validateTextDocument(textDocument: TextDocument): void { message: `Missing END CASE statement`, source: "MV Basic" }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: line.lineNumber, character: 0 }, - end: { line: line.lineNumber, character: line.lineOfCode.length } - } - }, - message: "Missing END CASE statement" - } - ]; - } diagnostics.push(diagnosic); } } @@ -471,7 +440,7 @@ function validateTextDocument(textDocument: TextDocument): void { // find the innermost for for (var i = 0; i < lines.length && problems < maxNumberOfProblems; i++) { let line = lines[i]; - if (rStartLoop.test(line.lineOfCode.trim()) == true) { + if (rStartLoop.test(line.lineOfCode)) { noLoop--; } if (noLoop == 0) { @@ -484,20 +453,6 @@ function validateTextDocument(textDocument: TextDocument): void { message: `Missing REPEAT statement`, source: "MV Basic" }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: line.lineNumber, character: 0 }, - end: { line: line.lineNumber, character: line.lineOfCode.length } - } - }, - message: "Missing REPEAT statement" - } - ]; - } diagnostics.push(diagnosic); } } @@ -514,165 +469,75 @@ function validateTextDocument(textDocument: TextDocument): void { end: { line: lastLine.lineNumber, character: lastLine.lineOfCode.length } }, message: `Missing END, END CASE or REPEAT statements`, - source: "ex" + source: "MV Basic" }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: lastLine.lineNumber, character: 0 }, - end: { line: lastLine.lineNumber, character: lastLine.lineOfCode.length } - } - }, - message: "One of the code blocks is missing an END" - } - ]; - } diagnostics.push(diagnosic); } // Missing GO, GO TO, GOTO, GOSUB // regex to check for goto/gosub in a line - let rGoto = new RegExp("((gosub|goto|go|go to)\\s\\w+)", "ig"); + let rGoto = new RegExp("(^| )(go to|goto|go|gosub)(\\s+.*)", "i"); + for (var i = 0; i < lines.length && problems < maxNumberOfProblems; i++) { let line = lines[i]; - // ignore comment lines - if (rComment.test(line.lineOfCode.trim()) == true) { - continue; - } - // remove trailing comments - if (tComment.test(line.lineOfCode.trim()) == true) { - let comment = tComment.exec(line.lineOfCode.trim()); - if (comment !== null) { - line.lineOfCode = line.lineOfCode.trim().replace(comment[0], ""); - } - } - lComment.lastIndex = 0; - if (lComment.test(line.lineOfCode.trim()) === true) { - let comment = trailingComment.exec(line.lineOfCode.trim()); - if (comment !== null) { - line.lineOfCode = line.lineOfCode.trim().replace(comment[0], ""); - } - } - // remove any quoted string - qStrings.lastIndex = 0; - while (qStrings.test(line.lineOfCode) == true) { - qStrings.lastIndex = 0; - let str = qStrings.exec(line.lineOfCode); - if (str !== null) { - line.lineOfCode = line.lineOfCode.replace(str[0], ""); - } - qStrings.lastIndex = 0; - } - + let labelName = ""; // check any gosubs or goto's to ensure label is present - rGoto.lastIndex = 0; - if (rGoto.test(line.lineOfCode.trim()) == true) { - while (line.lineOfCode.indexOf(",") > -1) { - line.lineOfCode = line.lineOfCode.replace(",", " "); + let text = line.lineOfCode; + text.split(rGoto) + let arrLabels = rGoto.exec(text); + if (arrLabels == null) { continue } + text = arrLabels[3]; + let labels = text.split(","); + + for (let ndx = 0; ndx < labels.length; ndx++) { + const item = labels[ndx]; + labelName = getWord(item, 1); + if (labelName.toLocaleLowerCase() == "to") { + labelName = getWord(item, 2); } - let values = line.lineOfCode.replace(";", " ").split(" "); - let labelName = ""; + let checkLabel = ""; - let cnt = 0; - values.forEach(function (value) { - cnt++; + let labelMatch = LabelList.find(label => label.LabelName === labelName); + if (labelMatch) { + // set the referened flag + labelMatch.Referenced = true; if ( - value.toLowerCase() == "goto" || - value.toLowerCase() == "gosub" || - value.toLowerCase() == "go" + labelMatch.Level != RowLevel[i] && + labelMatch.Level > 1 && + ignoreGotoScope === false ) { - while (cnt < values.length) { - labelName = values[cnt] - .replace(";", "") - .replace("*", "") - .replace(":", ""); - if (labelName === "to") { - cnt++; - labelName = values[cnt] - .replace(";", "") - .replace("*", "") - .replace(":", ""); - } - if (labelName) { - let labelMatch = LabelList.find(label => label.LabelName === labelName); - if (labelMatch) { - // set the referened flag - labelMatch.Referenced = true; - if ( - labelMatch.Level != RowLevel[i] && - labelMatch.Level > 1 && - ignoreGotoScope === false - ) { - // jumping into or out of a loop - let index = line.lineOfCode.indexOf(labelName); - let diagnosic: Diagnostic = { - severity: DiagnosticSeverity.Error, - range: { - start: { line: line.lineNumber, character: index }, - end: { line: line.lineNumber, character: index + labelName.length } - }, - message: `${labelName} is trying to go out of scope`, - source: "ex" - }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: line.lineNumber, character: index }, - end: { - line: line.lineNumber, - character: index + labelName.length - } - } - }, - message: - "Invalid GOTO or GOSUB, jumping into/out of a block" - } - ]; - } - diagnostics.push(diagnosic); - } - } else { - let index = line.lineOfCode.indexOf(labelName); - let diagnosic: Diagnostic = { - severity: DiagnosticSeverity.Error, - range: { - start: { line: line.lineNumber, character: index }, - end: { line: line.lineNumber, character: index + labelName.length } - }, - message: `${labelName} is not defined as a label in the program`, - source: "MV Basic" - }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: line.lineNumber, character: index }, - end: { line: line.lineNumber, character: index + labelName.length } - } - }, - message: "Invalid GOTO or GOSUB" - } - ]; - } - diagnostics.push(diagnosic); - - connection.console.log( - `[Server(${process.pid})] CheckLabel: ${checkLabel} + MatchedLabel: ${labelMatch}` - ); - } - } - cnt++; - } + // jumping into or out of a loop + let index = line.lineOfCode.indexOf(labelName); + let diagnosic: Diagnostic = { + severity: DiagnosticSeverity.Warning, + range: { + start: { line: line.lineNumber, character: index }, + end: { line: line.lineNumber, character: index + labelName.length } + }, + message: `${labelName} goes out of scope. Invalid GOTO or GOSUB`, + source: "MV Basic" + }; + diagnostics.push(diagnosic); } - }); + } else { + let index = line.lineOfCode.indexOf(labelName); + let diagnosic: Diagnostic = { + severity: DiagnosticSeverity.Error, + range: { + start: { line: line.lineNumber, character: index }, + end: { line: line.lineNumber, character: index + labelName.length } + }, + message: `Label ${labelName} not found! - Invalid GOTO or GOSUB`, + source: "MV Basic" + }; + diagnostics.push(diagnosic); + + if (logLevel) { + connection.console.log( + `[Server(${process.pid})] CheckLabel: ${checkLabel} + MatchedLabel: ${labelMatch}` + ); + } + } } } @@ -685,27 +550,9 @@ function validateTextDocument(textDocument: TextDocument): void { start: { line: label.LineNumber, character: 0 }, end: { line: label.LineNumber, character: label.LabelName.length } }, - message: `${label.LabelName} is not referenced in the program`, + message: `Label ${label.LabelName} is not referenced`, source: "MV Basic" }; - if (shouldSendDiagnosticRelatedInformation) { - diagnosic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: { - start: { line: label.LineNumber, character: 0 }, - end: { - line: label.LineNumber, - character: label.LabelName.length - } - } - }, - message: - "Label not referenced in the program; consider removing if unnecessary." - } - ]; - } diagnostics.push(diagnosic); } }); @@ -719,6 +566,8 @@ connection.listen(); // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. +let shouldSendDiagnosticRelatedInformation: boolean | undefined = false; +if (shouldSendDiagnosticRelatedInformation) { null } connection.onInitialize( (_params): InitializeResult => { shouldSendDiagnosticRelatedInformation = @@ -812,6 +661,8 @@ connection.onCompletion( if ( statement.toLocaleLowerCase() === "gosub" || + statement.toLocaleLowerCase() === "go" || + statement.toLocaleLowerCase() === "go to" || statement.toLocaleLowerCase() === "goto" ) { for (let i = 0; i < LabelList.length; i++) { @@ -930,7 +781,7 @@ connection.onDocumentSymbol(params => { for (let i = 0; i < lines.length; i++) { let line = lines[i]; rGoto.lastIndex = 0; - if (rGoto.test(line.trim()) === true) { + if (rGoto.test(line)) { let words = line.trim().split(" "); for (let j = 0; j < words.length; j++) { if (words[j].toLowerCase() === "call") { @@ -945,7 +796,7 @@ connection.onDocumentSymbol(params => { } } } - if (rInclude.test(line.trim()) === true) { + if (rInclude.test(line)) { let words = line.trim().split(" "); let li: Range = Range.create(i, 0, i, 9999); let includeName = words[1]; @@ -1057,7 +908,7 @@ connection.onDefinition(params => { ); for (var i = 0; i < lines.length; i++) { line = lines[i]; - if (rLabel.test(line.trim()) == true) { + if (rLabel.test(line)) { let label = ""; if (line !== null) { let labels = rLabel.exec(line.trim());