Added VSCode extension to format Knossos IR

Add comment

Fix issues with comments and strings and added test

Added launch.json and tasks.json

added knossos_ir_formatter.ts

fix lost indent

Handle let and assert correctly

Handle def args and pr correctly

fix indent etc

refactor into stack

Handle whitespace at the end

Fix install.ps1 and remove tsc

Added .js files

cleanup and comment
This commit is contained in:
Ryota Tomioka 2019-10-15 16:19:00 +01:00
Родитель 61888fd07d
Коммит bda7e74f2a
11 изменённых файлов: 733 добавлений и 13 удалений

22
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,22 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/etc/ks-vscode/"
],
"outFiles": [
"${workspaceFolder}/etc/ks-vscode/out/**/*.js"
],
"preLaunchTask": "npm-watch"
}
]
}

21
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,21 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "npm-watch",
"command": "npm run watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}/etc/ks-vscode"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

Просмотреть файл

@ -21,6 +21,35 @@ This extension contributes the following settings:
--->
## How to build the formatter extension
1. Install npm from https://nodejs.org/en/download/
2. In `./etc/ks-vscode` run
```
npm install
```
3. Then run
```
npm run compile
```
4. Copy the files in `./etc/ks-vscode/` to `C:\Users\<user>\.vscode\extensions\knossos-vscode-0.01`
```
.
├── package.json
├── language-configuration.json
├── out
| ├── extension.js
| └── knossos_ir_formatter.js
└── syntaxes
└── Knossos.tmLanguage
```
5. Start a new instance of VS Code. Open a `.ks` file. Make sure that VS Code auto detects Knossos IR. Then try "Format Document" (`shift` + `alt` + `F`).
## Known Issues
None yet.

Просмотреть файл

@ -10,7 +10,9 @@ $manifest = echo `
language-configuration.json `
package.json `
README.md `
syntaxes\Knossos.tmLanguage
syntaxes\Knossos.tmLanguage `
out\knossos_ir_formatter.js `
out\extension.js
write-host "ks-vscode: Deleting $extensions_dst"
Remove-Item -force -rec $extensions_dst

Просмотреть файл

@ -0,0 +1,23 @@
"use strict";
/*
Knossos IR formatter VSCode extension
Original extension vscode-lisp-formatter was developed by Jacob Clark and licensed under the MIT license
https://github.com/imjacobclark/vscode-lisp-formatter
*/
Object.defineProperty(exports, "__esModule", { value: true });
const vscode = require("vscode");
const knossos_ir_formatter_1 = require("./knossos_ir_formatter");
function getFullDocRange(document) {
return document.validateRange(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(Number.MAX_VALUE, Number.MAX_VALUE)));
}
function activate(context) {
vscode.languages.registerDocumentFormattingEditProvider('ks-lisp', {
provideDocumentFormattingEdits(document) {
return [vscode.TextEdit.replace(getFullDocRange(document), knossos_ir_formatter_1.formatKnossosIR(document.getText()))];
}
});
}
exports.activate = activate;
//# sourceMappingURL=extension.js.map

Просмотреть файл

@ -0,0 +1,221 @@
"use strict";
/*
Knossos IR formatter VSCode extension
Original extension vscode-lisp-formatter was developed by Jacob Clark and licensed under the MIT license
https://github.com/imjacobclark/vscode-lisp-formatter
*/
Object.defineProperty(exports, "__esModule", { value: true });
function insertNewline(state) {
const indent = " ".repeat(2 * (getIndent(state) + 1));
// don't insert an extra line break if it is already on a new line
if (!state.newLine) {
state.formattedDocument += "\n" + indent;
return state;
}
// just add the indent
state.formattedDocument += indent;
state.newLine = false;
return state;
}
function checkContext(state, contextOp, positionLimit) {
if (state.stack.length == 0) {
return false;
}
let index = -1;
for (var i = state.stack.length - 1; i >= 0; i--) {
if (state.stack[i].op == contextOp) {
index = i;
break;
}
}
return index >= 0 && state.stack[index].argIndex <= positionLimit;
}
function getIndent(state) {
return (state.stack.length > 0) ? state.stack[state.stack.length - 1].indent : 0;
}
function needLineBreak(state) {
let currentOp = "";
let opIndex = -1;
if (state.stack.length > 0) {
currentOp = state.stack[state.stack.length - 1].op;
opIndex = state.stack[state.stack.length - 1].argIndex;
}
const insideIfPred = checkContext(state, "if", 1);
const insideAssertPred = checkContext(state, "assert", 1);
const insideDeltaVecDim = checkContext(state, "deltaVec", 2);
const insideIndexDim = checkContext(state, "index", 1);
const insideLetBind = checkContext(state, "let", 1);
const insideDefArgs = checkContext(state, "def", 3);
return currentOp == "lam" && opIndex == 2
|| !insideIfPred && !insideAssertPred && !insideDeltaVecDim && !insideIndexDim
&& (currentOp == "add"
|| currentOp == "sub"
|| currentOp == "mul"
|| currentOp == "div")
|| currentOp == "if" && opIndex >= 2
|| currentOp == "assert" && opIndex >= 2
|| currentOp == "tuple"
|| currentOp == "deltaVec" && opIndex == 3
|| currentOp == "let"
|| currentOp == "" && insideLetBind
|| currentOp == "" && insideDefArgs
|| currentOp == "def"
|| currentOp == "pr";
}
function formatOpenList(state, token) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
if (!state.string && !state.comment) {
// Increment the argdIndex if no whitespace before opening parenthesis
if (state.stack.length > 0 && !state.whitespaceEmitted) {
state.stack[state.stack.length - 1].argIndex++;
}
const isOnNewLine = needLineBreak(state);
if (isOnNewLine) {
insertNewline(state);
}
state.formattedDocument += token;
state.openLists++;
const currentIndent = getIndent(state);
state.stack.push({
op: "",
argIndex: 0,
indent: (isOnNewLine) ? currentIndent + 1 : currentIndent
});
state.whitespaceEmitted = false;
}
else {
state.formattedDocument += token;
}
return state;
}
function formatCloseList(state, token) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
if (!state.string && !state.comment) {
state.formattedDocument += token;
state.openLists--;
state.stack.pop();
state.whitespaceEmitted = false;
}
else {
state.formattedDocument += token;
}
return state;
}
function formatNewLine(state, token) {
state.newLine = true;
state.comment = false;
if (state.stack.length > 0 && !state.whitespaceEmitted) {
state.whitespaceEmitted = true;
state.stack[state.stack.length - 1].argIndex++;
}
state.formattedDocument += token;
return state;
}
function formatWhitespace(state, token) {
const charIsInsideACommentOrString = state.comment || state.string;
// ignore repeated whitespace characters
if (charIsInsideACommentOrString || state.stack.length > 0 && !state.whitespaceEmitted) {
state.formattedDocument += token;
// increase the argIndex when inside an array
if (!charIsInsideACommentOrString) {
state.whitespaceEmitted = true;
state.stack[state.stack.length - 1].argIndex++;
}
}
return state;
}
function formatComment(state, token) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
else if (!state.string) {
state.comment = true;
}
state.formattedDocument += token;
return state;
}
function escapeFormatter(state, token) {
state.escaped = !state.escaped;
state.formattedDocument += token;
return state;
}
function stringFormatter(state, token) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
else {
state.string = !state.string;
}
// Reset whitespaceEmitted
state.whitespaceEmitted = false;
state.formattedDocument += token;
return state;
}
function formatKnossosIR(document) {
let state = {
document: document,
formattedDocument: "",
openLists: 0,
comment: false,
escaped: false,
string: false,
newLine: false,
array: false,
whitespaceEmitted: false,
stack: []
};
let formatters = {
"(": formatOpenList,
")": formatCloseList,
"\r": formatNewLine,
"\n": formatNewLine,
" ": formatWhitespace,
"\t": formatWhitespace,
";": formatComment,
"\\": escapeFormatter,
"\"": stringFormatter
};
for (var i = 0; i < state.document.length; i++) {
const cursor = state.document.charAt(i);
const formatter = formatters[cursor];
if (formatter) {
state = formatter(state, cursor);
}
else {
// Uncommenting this will insert line breaks even when the subexpression does not start from parenthesis
// but I found it a bit too verbose. The current approach of reading one character at a time cannot look
// ahead and a proper parsing followed by pretty printing will solve this issue.
// if (state.whitespaceEmitted && !state.comment && !state.string && needLineBreak(state)) {
// insertNewline(state);
//}
state.formattedDocument += cursor;
if (state.stack.length > 0) {
let currentOp = state.stack[state.stack.length - 1];
if (currentOp.argIndex == 0) {
currentOp.op += cursor;
}
}
state.newLine = false;
// reset the whitespceEmitted variable if not in string or comment
if (!state.comment && !state.string) {
state.whitespaceEmitted = false;
}
if (state.escaped) {
state.escaped = false;
}
}
}
return state.formattedDocument;
}
exports.formatKnossosIR = formatKnossosIR;
//# sourceMappingURL=knossos_ir_formatter.js.map

Просмотреть файл

@ -9,17 +9,47 @@
"categories": [
"Programming Languages"
],
"activationEvents": [
"onLanguage:ks-lisp"
],
"main": "./out/extension",
"contributes": {
"languages": [{
"id": "ks-lisp",
"aliases": ["Knossos IR", "ks-lisp"],
"extensions": [".ks",".kso"],
"configuration": "./language-configuration.json"
}],
"grammars": [{
"language": "ks-lisp",
"scopeName": "source.ks-lisp",
"path": "./syntaxes/Knossos.tmLanguage"
}]
"languages": [
{
"id": "ks-lisp",
"aliases": [
"Knossos IR",
"ks-lisp"
],
"extensions": [
".ks",
".kso"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "ks-lisp",
"scopeName": "source.ks-lisp",
"path": "./syntaxes/Knossos.tmLanguage"
}
]
},
"scripts": {
"compile": "tsc -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"watch": "tsc -watch -p ./",
"test": "mocha -u tdd out/test"
},
"devDependencies": {
"@types/assert": "^1.4.3",
"@types/mocha": "",
"@types/node": "",
"vscode": "^1.1.22"
},
"dependencies": {
"mocha": "^6.2.1",
"typescript": "^3.3.3"
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
/*
Knossos IR formatter VSCode extension
Original extension vscode-lisp-formatter was developed by Jacob Clark and licensed under the MIT license
https://github.com/imjacobclark/vscode-lisp-formatter
*/
import * as vscode from 'vscode';
import { formatKnossosIR } from './knossos_ir_formatter'
function getFullDocRange(document: vscode.TextDocument): vscode.Range {
return document.validateRange(
new vscode.Range(
new vscode.Position(0, 0),
new vscode.Position(Number.MAX_VALUE, Number.MAX_VALUE)
)
);
}
export function activate(context: vscode.ExtensionContext) {
vscode.languages.registerDocumentFormattingEditProvider('ks-lisp', {
provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] {
return [vscode.TextEdit.replace(
getFullDocRange(document),
formatKnossosIR(document.getText()))];
}
});
}

Просмотреть файл

@ -0,0 +1,251 @@
/*
Knossos IR formatter VSCode extension
Original extension vscode-lisp-formatter was developed by Jacob Clark and licensed under the MIT license
https://github.com/imjacobclark/vscode-lisp-formatter
*/
function insertNewline(state: any) {
const indent = " ".repeat(2 * (getIndent(state) + 1));
// don't insert an extra line break if it is already on a new line
if (!state.newLine) {
state.formattedDocument += "\n" + indent;
return state;
}
// just add the indent
state.formattedDocument += indent;
state.newLine = false;
return state;
}
function checkContext(state: any, contextOp: string, positionLimit: number) {
if (state.stack.length == 0) {
return false;
}
let index = -1;
for (var i = state.stack.length - 1; i >= 0; i--) {
if (state.stack[i].op == contextOp) {
index = i;
break;
}
}
return index >= 0 && state.stack[index].argIndex <= positionLimit;
}
function getIndent(state: any) {
return (state.stack.length > 0) ? state.stack[state.stack.length-1].indent : 0;
}
function needLineBreak(state: any) {
let currentOp = "";
let opIndex = -1;
if (state.stack.length > 0) {
currentOp = state.stack[state.stack.length-1].op;
opIndex = state.stack[state.stack.length-1].argIndex;
}
const insideIfPred = checkContext(state, "if", 1);
const insideAssertPred = checkContext(state, "assert", 1);
const insideDeltaVecDim = checkContext(state, "deltaVec", 2);
const insideIndexDim = checkContext(state, "index", 1);
const insideLetBind = checkContext(state, "let", 1);
const insideDefArgs = checkContext(state, "def", 3);
return currentOp == "lam" && opIndex == 2
|| !insideIfPred && !insideAssertPred && !insideDeltaVecDim && !insideIndexDim
&& (currentOp == "add"
|| currentOp == "sub"
|| currentOp == "mul"
|| currentOp == "div")
|| currentOp == "if" && opIndex >= 2
|| currentOp == "assert" && opIndex >= 2
|| currentOp == "tuple"
|| currentOp == "deltaVec" && opIndex == 3
|| currentOp == "let"
|| currentOp == "" && insideLetBind
|| currentOp == "" && insideDefArgs
|| currentOp == "def"
|| currentOp == "pr";
}
function formatOpenList(state: any, token: string) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
if (!state.string && !state.comment) {
// Increment the argdIndex if no whitespace before opening parenthesis
if (state.stack.length > 0 && !state.whitespaceEmitted) {
state.stack[state.stack.length-1].argIndex++;
}
const isOnNewLine = needLineBreak(state);
if (isOnNewLine) {
insertNewline(state);
}
state.formattedDocument += token;
state.openLists++;
const currentIndent = getIndent(state);
state.stack.push({
op: "",
argIndex: 0,
indent: (isOnNewLine) ? currentIndent + 1 : currentIndent
});
state.whitespaceEmitted = false;
} else {
state.formattedDocument += token;
}
return state;
}
function formatCloseList(state: any, token: string) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
}
if (!state.string && !state.comment) {
state.formattedDocument += token;
state.openLists--;
state.stack.pop();
state.whitespaceEmitted = false;
} else {
state.formattedDocument += token;
}
return state;
}
function formatNewLine(state: any, token: string) {
state.newLine = true;
state.comment = false;
if (state.stack.length > 0 && !state.whitespaceEmitted) {
state.whitespaceEmitted = true;
state.stack[state.stack.length-1].argIndex++;
}
state.formattedDocument += token;
return state;
}
function formatWhitespace(state: any, token: string) {
const charIsInsideACommentOrString = state.comment || state.string;
// ignore repeated whitespace characters
if (charIsInsideACommentOrString || state.stack.length > 0 && !state.whitespaceEmitted) {
state.formattedDocument += token;
// increase the argIndex when inside an array
if (!charIsInsideACommentOrString) {
state.whitespaceEmitted = true;
state.stack[state.stack.length-1].argIndex++;
}
}
return state;
}
function formatComment(state: any, token: string) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
} else if (!state.string) {
state.comment = true;
}
state.formattedDocument += token;
return state;
}
function escapeFormatter(state: any, token: string) {
state.escaped = !state.escaped;
state.formattedDocument += token;
return state;
}
function stringFormatter(state: any, token: string) {
const charIsEscaped = state.escaped;
if (charIsEscaped) {
state.escaped = false;
} else {
state.string = !state.string;
}
// Reset whitespaceEmitted
state.whitespaceEmitted = false;
state.formattedDocument += token;
return state;
}
export function formatKnossosIR(document: string) {
let state = {
document: document,
formattedDocument: "",
openLists: 0,
comment: false,
escaped: false,
string: false,
newLine: false,
array: false,
whitespaceEmitted: false,
stack: [] as {op: string, argIndex: number, indent: number}[]
}
let formatters: any = {
"(": formatOpenList,
")": formatCloseList,
"\r": formatNewLine,
"\n": formatNewLine,
" ": formatWhitespace,
"\t": formatWhitespace,
";": formatComment,
"\\": escapeFormatter,
"\"": stringFormatter
}
for (var i = 0; i < state.document.length; i++) {
const cursor = state.document.charAt(i)
const formatter = formatters[cursor];
if (formatter) {
state = formatter(state, cursor);
} else {
// Uncommenting this will insert line breaks even when the subexpression does not start from parenthesis
// but I found it a bit too verbose. The current approach of reading one character at a time cannot look
// ahead and a proper parsing followed by pretty printing will solve this issue.
// if (state.whitespaceEmitted && !state.comment && !state.string && needLineBreak(state)) {
// insertNewline(state);
//}
state.formattedDocument += cursor;
if (state.stack.length > 0) {
let currentOp = state.stack[state.stack.length-1];
if (currentOp.argIndex == 0) {
currentOp.op += cursor;
}
}
state.newLine = false;
// reset the whitespceEmitted variable if not in string or comment
if (!state.comment && !state.string) {
state.whitespaceEmitted = false;
}
if (state.escaped) {
state.escaped = false;
}
}
}
return state.formattedDocument;
}

Просмотреть файл

@ -0,0 +1,73 @@
import * as assert from 'assert'
import { formatKnossosIR } from '../knossos_ir_formatter'
suite('Knossos IR Formatter Extension Tests', function() {
// Defines a Mocha unit test
test('Correctly handles comments and strings', function() {
const formattedDocument = formatKnossosIR(`
; comment
(def "this is a test" (Vec m (Vec n Float)) ((m : Float) (n : Float)) ; comment with whitespace
((build " " (lam (mi : Integer) ; (()) comment with parentheses
(build " " (lam (ni : Integer) ")))"))))))`);
const expectedFormattedDocument = `
; comment
(def "this is a test"
(Vec m (Vec n Float))
(
(m : Float)
(n : Float)) ; comment with whitespace
((build " " (lam (mi : Integer) ; (()) comment with parentheses
(build " " (lam (ni : Integer) ")))"))))))`;
assert.equal(formattedDocument, expectedFormattedDocument);
});
test('Do not lose indent after newline', function() {
const formattedDocument = formatKnossosIR(`
(build n (lam (i : Integer)
(to_float i)))`);
const expectedFormattedDocument = `
(build n (lam (i : Integer)
(to_float i)))`;
assert.equal(formattedDocument, expectedFormattedDocument);
});
test('Handle let correctly', function() {
const formattedDocument = formatKnossosIR(`
(let ((a 1.0) (b 2.0) (c (add a b))) (pr c))`);
const expectedFormattedDocument = `
(let
(
(a 1.0)
(b 2.0)
(c (add a b)))
(pr c))`;
assert.equal(formattedDocument, expectedFormattedDocument);
});
test('Handle assert correctly', function() {
const formattedDocument = formatKnossosIR(`
(assert (gt (add (sub x 1.0) 1.0) 0.0) (div 1.0 x))`);
const expectedFormattedDocument = `
(assert (gt (add (sub x 1.0) 1.0) 0.0)
(div 1.0 x))`;
assert.equal(formattedDocument, expectedFormattedDocument);
});
test('Handle pr correctly', function() {
const formattedDocument = formatKnossosIR(`
(pr (add x y) (sub x y))`);
const expectedFormattedDocument = `
(pr
(add x y)
(sub x y))`;
assert.equal(formattedDocument, expectedFormattedDocument);
});
test('Handle trailing whitespace', function() {
const formattedDocument = formatKnossosIR(`
(add x y) `);
const expectedFormattedDocument = `
(add x y)`
assert.equal(formattedDocument, expectedFormattedDocument);
});
});

Просмотреть файл

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6"
],
"sourceMap": true,
"strict": true,
"noUnusedLocals": true,
},
"exclude": [
"node_modules",
".vscode-test",
".vscode"
]
}