From 83e34019030db98734d3f748f320199475d5333e Mon Sep 17 00:00:00 2001 From: David Gileadi Date: Mon, 17 Oct 2016 11:55:40 -0700 Subject: [PATCH] Start supporting basic hover and definitions --- .eslintrc.json | 24 ++++++++++ .gitignore | 1 + .vscode/launch.json | 22 +++++++++ .vscode/settings.json | 4 ++ .vscodeignore | 7 +++ extension.js | 17 +++++++ jsconfig.json | 12 +++++ language-definitions.json | 77 +++++++++++++++++++++++++++++++ mumps-definition-provider.js | 68 ++++++++++++++++++++++++++++ mumps-hover-provider.js | 43 ++++++++++++++++++ mumps-language-token.js | 87 ++++++++++++++++++++++++++++++++++++ package.json | 15 +++++++ 12 files changed, 377 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscodeignore create mode 100644 extension.js create mode 100644 jsconfig.json create mode 100644 language-definitions.json create mode 100644 mumps-definition-provider.js create mode 100644 mumps-hover-provider.js create mode 100644 mumps-language-token.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f6d8233 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "env": { + "browser": false, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "extends": ["esnext"], + "rules": { + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "warn", + "no-unreachable": "warn", + "no-unused-vars": "warn", + "constructor-super": "warn", + "valid-typeof": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..12260af --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +// A launch configuration that launches the extension inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false + }, + { + "name": "Test", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..902014b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..499648c --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,7 @@ +.vscode/** +.vscode-test/** +test/** +.gitignore +jsconfig.json +vsc-extension-quickstart.md +.eslintrc.json diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..ebb2686 --- /dev/null +++ b/extension.js @@ -0,0 +1,17 @@ +let vscode = require('vscode'); +let MumpsHoverProvider = require('./mumps-hover-provider').MumpsHoverProvider; +let MumpsDefinitionProvider = require('./mumps-definition-provider').MumpsDefinitionProvider; + +function activate(context) { + context.subscriptions.push( + vscode.languages.registerHoverProvider( + 'mumps', new MumpsHoverProvider())); + context.subscriptions.push( + vscode.languages.registerDefinitionProvider( + 'mumps', new MumpsDefinitionProvider())); +} +exports.activate = activate; + +function deactivate() { +} +exports.deactivate = deactivate; \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..b7caa7d --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "lib": [ + "es6" + ] + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/language-definitions.json b/language-definitions.json new file mode 100644 index 0000000..4de23fa --- /dev/null +++ b/language-definitions.json @@ -0,0 +1,77 @@ +[ +{ "name": "BREAK", "type": "command", "abbreviation": "B", "description": "suspends execution or exits a block (non- standard extension)" }, +{ "name": "CLOSE", "type": "command", "abbreviation": "C", "description": "release an I/O device" }, +{ "name": "DATABASE", "type": "command", "description": "set global array database (non-standard extension)" }, +{ "name": "DO", "type": "command", "abbreviation": "D", "description": "execute a program, section of code or block" }, +{ "name": "ELSE", "type": "command", "abbreviation": "E", "description": "conditional execution based on $test" }, +{ "name": "FOR", "type": "command", "abbreviation": "F", "description": "iterative execution of a line or block" }, +{ "name": "GOTO", "type": "command", "abbreviation": "G", "description": "transfer of control to a label or program" }, +{ "name": "HALT", "type": "command", "abbreviation": "H", "description": "terminate execution" }, +{ "name": "HANG", "type": "command", "abbreviation": "H", "description": "delay execution for a specified period of time" }, +{ "name": "HTML", "type": "command", "description": "write line to web server (non-standard extension)" }, +{ "name": "IF", "type": "command", "abbreviation": "I", "description": "conditional execution of remainder of line" }, +{ "name": "JOB", "type": "command", "abbreviation": "J", "description": "create an independent process" }, +{ "name": "LOCK", "type": "command", "abbreviation": "L", "description": "exclusive access/release named resource" }, +{ "name": "KILL", "type": "command", "abbreviation": "K", "description": "delete a local or global variable" }, +{ "name": "MERGE", "type": "command", "abbreviation": "M", "description": "copy arrays" }, +{ "name": "NEW", "type": "command", "abbreviation": "N", "description": "create new copies of local variables" }, +{ "name": "OPEN", "type": "command", "abbreviation": "O", "description": "obtain ownership of a device" }, +{ "name": "QUIT", "type": "command", "abbreviation": "Q", "description": "end a for loop or exit a block" }, +{ "name": "READ", "type": "command", "abbreviation": "R", "description": "read from a device" }, +{ "name": "SET", "type": "command", "abbreviation": "S", "description": "assign a value to a global or local variable" }, +{ "name": "SHELL", "type": "command", "description": "execute a command shell (non-standard extension)" }, +{ "name": "SQL", "type": "command", "description": "execute an SQL statement (non-standard extension)" }, +{ "name": "TCOMMIT", "type": "command", "abbreviation": "TC", "description": "commit a transaction" }, +{ "name": "TRESTART", "type": "command", "abbreviation": "TRE", "description": "roll back / restart a transaction" }, +{ "name": "TROLLBACK", "type": "command", "abbreviation": "TRO", "description": "roll back a transaction" }, +{ "name": "TSTART", "type": "command", "abbreviation": "TS", "description": "begin a transaction" }, +{ "name": "USE", "type": "command", "abbreviation": "U", "description": "select which device to read/write" }, +{ "name": "VIEW", "type": "command", "abbreviation": "V", "description": "implementation defined" }, +{ "name": "WRITE", "type": "command", "abbreviation": "W", "description": "write to device" }, +{ "name": "XECUTE", "type": "command", "abbreviation": "X", "description": "dynamically execute strings" }, + +{ "name": "$DEVICE", "type": "variable", "abbreviation": "$D", "description": "status of current device" }, +{ "name": "$ECODE", "type": "variable", "abbreviation": "$EC", "description": "list of error codes" }, +{ "name": "$ESTACK", "type": "variable", "abbreviation": "$ES", "description": "number of stack levels" }, +{ "name": "$ETRAP", "type": "variable", "abbreviation": "$ET", "description": "code to execute on error" }, +{ "name": "$HOROLOG", "type": "variable", "abbreviation": "$H", "description": "days,seconds time stamp" }, +{ "name": "$IO", "type": "variable", "abbreviation": "$I", "description": "current IO unit" }, +{ "name": "$JOB", "type": "variable", "abbreviation": "$J", "description": "current process ID" }, +{ "name": "$KEY", "type": "variable", "abbreviation": "$K", "description": "read command control code" }, +{ "name": "$PRINCIPAL", "type": "variable", "abbreviation": "$P", "description": "principal IO device" }, +{ "name": "$QUIT", "type": "variable", "abbreviation": "$Q", "description": "indicates how current process invoked." }, +{ "name": "$STACK", "type": "variable", "abbreviation": "$ST", "description": "current process stack level" }, +{ "name": "$STORAGE", "type": "variable", "abbreviation": "$S", "description": "amount of memory available" }, +{ "name": "$SYSTEM", "type": "variable", "abbreviation": "$SY", "description": "system ID" }, +{ "name": "$TEST", "type": "variable", "abbreviation": "$T", "description": "result of prior operation" }, +{ "name": "$TLEVEL", "type": "variable", "abbreviation": "$TL", "description": "number transactions in process" }, +{ "name": "$TRESTART", "type": "variable", "abbreviation": "$TR", "description": "number of restarts on current transaction" }, +{ "name": "$X", "type": "variable", "abbreviation": "$X", "description": "position of horizontal cursor" }, +{ "name": "$Y", "type": "variable", "abbreviation": "$Y", "description": "position of vertical cursor" }, + +{ "name": "$ASCII", "type": "function", "abbreviation": "$A", "description": "ASCII numeric code of a character", "parameters": [ + { "name": "VALUE", "type": "string", "description": "A string to get a code from" }, + { "name": "POS", "type": "number", "description": "(optional) The 1-based position of the character in VALUE; defaults to 1" } +], "returns": "The ASCII numeric code or `-1` if not found" }, +{ "name": "$CHAR", "type": "function", "abbreviation": "$C", "description": "ASCII character from numeric code" }, +{ "name": "$DATA", "type": "function", "abbreviation": "$D", "description": "determines variable's definition" }, +{ "name": "$EXTRACT", "type": "function", "abbreviation": "$E", "description": "extract a substring" }, +{ "name": "$FIND", "type": "function", "abbreviation": "$F", "description": "find a substring" }, +{ "name": "$FNUMBER", "type": "function", "abbreviation": "$FN", "description": "format a number" }, +{ "name": "$GET", "type": "function", "abbreviation": "$G", "description": "get default or actual value" }, +{ "name": "$JUSTIFY", "type": "function", "abbreviation": "$J", "description": "format a number or string" }, +{ "name": "$LENGTH", "type": "function", "abbreviation": "$L", "description": "determine string length" }, +{ "name": "$NAME", "type": "function", "abbreviation": "$NA", "description": "evaluate array reference" }, +{ "name": "$ORDER", "type": "function", "abbreviation": "$O", "description": "find next or previous node" }, +{ "name": "$PIECE", "type": "function", "abbreviation": "$P", "description": "extract substring based on pattern" }, +{ "name": "$QLENGTH", "type": "function", "abbreviation": "$QL", "description": "number of subscripts in an array reference" }, +{ "name": "$QSUBSCRIPT", "type": "function", "abbreviation": "$QS", "description": "value of specified subscript" }, +{ "name": "$QUERY", "type": "function", "abbreviation": "$Q", "description": "next array reference" }, +{ "name": "$RANDOM", "type": "function", "abbreviation": "$R", "description": "random number" }, +{ "name": "$REVERSE", "type": "function", "abbreviation": "$RE", "description": "string in reverse order" }, +{ "name": "$SELECT", "type": "function", "abbreviation": "$S", "description": "value of first true argument" }, +{ "name": "$STACK", "type": "function", "abbreviation": "$ST", "description": "stack information" }, +{ "name": "$TEXT", "type": "function", "abbreviation": "$T", "description": "string containing a line of code" }, +{ "name": "$TRANSLATE", "type": "function", "abbreviation": "$TR", "description": "translate characters in a string" }, +{ "name": "$VIEW", "type": "function", "abbreviation": "$V", "description": "implementation defined" } +] \ No newline at end of file diff --git a/mumps-definition-provider.js b/mumps-definition-provider.js new file mode 100644 index 0000000..8d3659a --- /dev/null +++ b/mumps-definition-provider.js @@ -0,0 +1,68 @@ +let fs = require('fs'); +let path = require('path'); +let vscode = require('vscode'); +let Location = vscode.Location; +let Position = vscode.Position; +let Uri = vscode.Uri; +let MumpsToken = require('./mumps-language-token').MumpsToken; +const EXTENSIONS = ['.M', '.INT', '.ZWR', '.m', '.int', '.zwr']; + +class MumpsDefinitionProvider { + provideDefinition(document, position) { + let token = new MumpsToken(document, position); + if (token.isLabelReference) { + let uri = token.labelProgram ? + siblingUri(document, token.labelProgram) : + document.uri; + let labelPosition = token.label ? + findLabelPositionInFile(uri, token.label, token.labelOffset) : + token.labelOffset ? + document.positionAt(token.labelOffset) : + new Position(0, 0); + return new Location(uri, labelPosition); + } + } +} +exports.MumpsDefinitionProvider = MumpsDefinitionProvider; + +function siblingUri(document, fileName) { + let siblingPath = path.resolve(document.uri.fsPath, '../' + fileName); + if (!fs.existsSync(siblingPath)) { + for (var extension of EXTENSIONS) { + let extendedPath = siblingPath + extension; + if (fs.existsSync(extendedPath)) { + siblingPath = extendedPath; + break; + } + } + } + return Uri.file(siblingPath); +} + +function findLabelPositionInFile(uri, label, offset) { + let line = offset || 0; + if (label) { + try { + let text = fs.readFileSync(uri.fsPath, 'utf8'); + let labelRe = new RegExp('^' + label + '[ \t\\()]', 'm'); + let result = labelRe.exec(text); + if (result) { + line += countLines(text, result.index); + } + } catch (e) {} + } + return new Position(line, 0); +} + +function countLines(text, index) { + if (index >= text.length) { + index = text.length - 1; + } + let re = /[\r\n]+/g; + let result; + let line = 0; + while ((result = re.exec(text)) && result.index >= 0 && result.index < index) { + ++line; + } + return line; +} diff --git a/mumps-hover-provider.js b/mumps-hover-provider.js new file mode 100644 index 0000000..24a876d --- /dev/null +++ b/mumps-hover-provider.js @@ -0,0 +1,43 @@ +let vscode = require('vscode'); +let Hover = vscode.Hover; +let MumpsToken = require('./mumps-language-token').MumpsToken; +const definitions = require('./language-definitions.json'); + +const definitionsByName = {}; +for (var definition of definitions) { + addDefinition(definition.name, definition); + if (definition.abbreviation) { + addDefinition(definition.abbreviation, definition); + } +} + +function addDefinition(name, definition) { + if (!definitionsByName[name]) { + definitionsByName[name] = [definition]; + } else { + definitionsByName[name].push(definition); + } +} + +class MumpsHoverProvider { + provideHover(document, position) { + let token = new MumpsToken(document, position); + + if (!token.mayBeCommand && !token.isIntrinsic) { + return; + } + + let definitions = definitionsByName[token.word.toUpperCase()]; + if (definitions) { + for (definition of definitions) { + if (token.isFunctionCall && definition.type !== 'function') { + continue; + } + var markdown = '**' + definition.name + '**: ' + definition.description; +// TODO: function parameters + return new Hover(markdown, token.range); + } + } + } +} +exports.MumpsHoverProvider = MumpsHoverProvider; diff --git a/mumps-language-token.js b/mumps-language-token.js new file mode 100644 index 0000000..0a6f78d --- /dev/null +++ b/mumps-language-token.js @@ -0,0 +1,87 @@ +let vscode = require('vscode'); +let Position = vscode.Position; +let Range = vscode.Range; + +class MumpsToken { + constructor(document, position) { + this.document = document; + this.position = position; + + this.range = document.getWordRangeAtPosition(position); + if (!this.range) { + return; + } + + this.word = document.getText(this.range); + if (!this.word) { + return; + } + + this.surroundWord = getWordWithSurrounds(document, this.range) || this.word; + + if (this.isIntrinsic) { + this.word = '$' + this.word; + } + } + + get mayBeCommand() { + if (!this.surroundWord) { + return false; + } + var lastChar = this.surroundWord.charAt(this.surroundWord.length - 1); + return isWhitespace(this.surroundWord.charAt(0)) && + (lastChar === ':' || + isWhitespace(lastChar) || + this.surroundWord.length === this.word.length + 1); // end-of-line + } + + get isIntrinsic() { + if (!this.surroundWord) { + return false; + } + return this.surroundWord.charAt(0) === '$'; + } + + get isFunctionCall() { + if (!this.surroundWord) { + return false; + } + return this.surroundWord.charAt(this.surroundWord.length - 1) === '('; + } + + get isLabelReference() { + if (this._isLabelReference === undefined) { + let line = this.document.lineAt(this.range.start); + let regex = new RegExp('[ \t](D|DO|G|GOTO)[ \t]+([%\\^\\+A-Z0-9]*' + this.word + '[%\\^\\+A-Z0-9]*)', 'i'); + let match = regex.exec(line.text); + this._isLabelReference = match !== null; + if (match) { + let fullLabel = match[2]; + let partsRegex = /([%A-Z][%A-Z0-9]*)?(\+\d+)?(\^[%A-Z][%A-Z0-9]*)?/gi; + let parts = partsRegex.exec(fullLabel); + this.label = parts[1]; + this.labelOffset = withoutFirstCharacter(parts[2]); + this.labelProgram = withoutFirstCharacter(parts[3]); + } + } + return this._isLabelReference; + } +} +exports.MumpsToken = MumpsToken; + +function getWordWithSurrounds(document, range) { + if (range.start.character <= 0) { + return; + } + let start = new Position(range.start.line, range.start.character - 1); + let end = new Position(range.end.line, range.end.character + 1); + return document.getText(new Range(start, end)); +} + +function isWhitespace(char) { + return /\s+/.test(char); +} + +function withoutFirstCharacter(string) { + return string ? string.substring(1) : string; +} \ No newline at end of file diff --git a/package.json b/package.json index 74599d3..34d0020 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,20 @@ "scopeName": "source.mumps", "path": "./syntaxes/mumps.tmLanguage" }] + }, + "activationEvents": [ + "onLanguage:mumps" + ], + "main": "./extension", + "scripts": { + "postinstall": "node ./node_modules/vscode/bin/install" + }, + "devDependencies": { + "typescript": "^2.0.3", + "vscode": "^1.0.0", + "mocha": "^2.3.3", + "eslint": "^3.6.0", + "@types/node": "^6.0.40", + "@types/mocha": "^2.2.32" } } \ No newline at end of file