diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..90ee7733 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..0897e978 --- /dev/null +++ b/.vscode/tasks.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/etc/ks-vscode/README.md b/etc/ks-vscode/README.md index 2cff07ff..b74b1ab5 100644 --- a/etc/ks-vscode/README.md +++ b/etc/ks-vscode/README.md @@ -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\\.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. diff --git a/etc/ks-vscode/install.ps1 b/etc/ks-vscode/install.ps1 index 99b23e66..66f1521a 100644 --- a/etc/ks-vscode/install.ps1 +++ b/etc/ks-vscode/install.ps1 @@ -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 diff --git a/etc/ks-vscode/out/extension.js b/etc/ks-vscode/out/extension.js new file mode 100644 index 00000000..86511bc5 --- /dev/null +++ b/etc/ks-vscode/out/extension.js @@ -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 \ No newline at end of file diff --git a/etc/ks-vscode/out/knossos_ir_formatter.js b/etc/ks-vscode/out/knossos_ir_formatter.js new file mode 100644 index 00000000..a327085e --- /dev/null +++ b/etc/ks-vscode/out/knossos_ir_formatter.js @@ -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 \ No newline at end of file diff --git a/etc/ks-vscode/package.json b/etc/ks-vscode/package.json index bcc80859..b8ce0f31 100644 --- a/etc/ks-vscode/package.json +++ b/etc/ks-vscode/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/etc/ks-vscode/src/extension.ts b/etc/ks-vscode/src/extension.ts new file mode 100644 index 00000000..ba7e1994 --- /dev/null +++ b/etc/ks-vscode/src/extension.ts @@ -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()))]; + } + }); +} diff --git a/etc/ks-vscode/src/knossos_ir_formatter.ts b/etc/ks-vscode/src/knossos_ir_formatter.ts new file mode 100644 index 00000000..4a7ca7bb --- /dev/null +++ b/etc/ks-vscode/src/knossos_ir_formatter.ts @@ -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; +} \ No newline at end of file diff --git a/etc/ks-vscode/src/test/extension.test.ts b/etc/ks-vscode/src/test/extension.test.ts new file mode 100644 index 00000000..ed8473f8 --- /dev/null +++ b/etc/ks-vscode/src/test/extension.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/etc/ks-vscode/tsconfig.json b/etc/ks-vscode/tsconfig.json new file mode 100644 index 00000000..e45578ee --- /dev/null +++ b/etc/ks-vscode/tsconfig.json @@ -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" + ] +} \ No newline at end of file