Skip to content

Commit

Permalink
Start supporting basic hover and definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
David Gileadi committed Oct 17, 2016
1 parent a7f537a commit 83e3401
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.vscode/**
.vscode-test/**
test/**
.gitignore
jsconfig.json
vsc-extension-quickstart.md
.eslintrc.json
17 changes: 17 additions & 0 deletions extension.js
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 12 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": [
"es6"
]
},
"exclude": [
"node_modules"
]
}
77 changes: 77 additions & 0 deletions language-definitions.json
Original file line number Diff line number Diff line change
@@ -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" }
]
68 changes: 68 additions & 0 deletions mumps-definition-provider.js
Original file line number Diff line number Diff line change
@@ -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;
}
43 changes: 43 additions & 0 deletions mumps-hover-provider.js
Original file line number Diff line number Diff line change
@@ -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;
87 changes: 87 additions & 0 deletions mumps-language-token.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 83e3401

Please sign in to comment.