diff --git a/package-lock.json b/package-lock.json index 897808e70..5aa6d3305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Go", - "version": "0.11.4", + "version": "0.11.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -912,6 +912,11 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "jsonc-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.1.0.tgz", + "integrity": "sha512-n9GrT8rrr2fhvBbANa1g+xFmgGK5X91KFeDwlKQ3+SJfmH5+tKv/M/kahx/TXOMflfWHKGKqKyfHQaLKTNzJ6w==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -1118,6 +1123,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -1498,6 +1509,21 @@ } } }, + "tree-sitter-cli": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.15.9.tgz", + "integrity": "sha512-jqeuo78srjRgYu4QjoPfLiYxBKSUGiv3/pZEaZZT2bB1fTQzaBS9mVtQvgAYTOC/fwY/K894Hb/LHqshmdopKw==", + "dev": true + }, + "tree-sitter-go": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.15.0.tgz", + "integrity": "sha512-booht80IETCTTj79Yeicr0UmH9DhZeg8IA58Cf8evuARatbebsUQdh4Zg49Ye+15zVD663/LM+NxkmnJLfq2Rw==", + "dev": true, + "requires": { + "nan": "^2.10.0" + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -1664,6 +1690,15 @@ "rimraf": "^2.6.3" } }, + "vscode-tree-sitter": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/vscode-tree-sitter/-/vscode-tree-sitter-0.1.24.tgz", + "integrity": "sha512-Q7742RUd02FTdhCZAlUUXcjIS3wm4Oiszc6hJKeuYwJbkMHi0+KwApuTYD/UktFSw0Djbb8SAgxfC8+qyDMH0A==", + "requires": { + "jsonc-parser": "^2.1.0", + "web-tree-sitter": "^0.15.5" + } + }, "web-request": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/web-request/-/web-request-1.0.7.tgz", @@ -1672,6 +1707,11 @@ "request": "^2.69.0" } }, + "web-tree-sitter": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.15.9.tgz", + "integrity": "sha512-1lf4lnmi8oxuEzI6gpUok2FQlHXOmV1iipltkQvmR785JWnUbjhw1sZnwSCkisQP+/g/aezpMGcW3mjz0uVhMw==" + }, "websocket-driver": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", diff --git a/package.json b/package.json index 6d4567ee8..0f8395fa2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "test": "node ./out/test/runTest.js", "lint": "node ./node_modules/tslint/bin/tslint --project tsconfig.json", "fix-lint": "node ./node_modules/tslint/bin/tslint --fix --project tsconfig.json", - "unit-test": "node ./node_modules/mocha/bin/_mocha -u tdd --timeout 5000 --colors ./out/test/unit" + "unit-test": "node ./node_modules/mocha/bin/_mocha -u tdd --timeout 5000 --colors ./out/test/unit", + "gen-parser": "tree-sitter build-wasm ./node_modules/tree-sitter-go" }, "extensionDependencies": [], "dependencies": { @@ -46,7 +47,9 @@ "vscode-debugprotocol": "^1.36.0", "vscode-extension-telemetry": "^0.1.2", "vscode-languageclient": "~5.2.1", - "web-request": "^1.0.7" + "vscode-tree-sitter": "^0.1.24", + "web-request": "^1.0.7", + "web-tree-sitter": "^0.15.9" }, "devDependencies": { "@types/fs-extra": "^8.0.0", @@ -56,6 +59,8 @@ "@types/semver": "^6.0.1", "@types/vscode": "^1.25.0", "fs-extra": "^8.1.0", + "tree-sitter-cli": "^0.15.9", + "tree-sitter-go": "^0.15.0", "glob": "^7.1.4", "mocha": "^6.2.0", "tslint": "^5.19.0", @@ -104,6 +109,11 @@ } ], "grammars": [ + { + "language": "go", + "scopeName": "source.go", + "path": "./syntaxes/go.tmGrammar.json" + }, { "language": "go.mod", "scopeName": "go.mod", diff --git a/parsers/README.md b/parsers/README.md new file mode 100644 index 000000000..973e35421 --- /dev/null +++ b/parsers/README.md @@ -0,0 +1,3 @@ +`tree-sitter-go.wasm` is an incremental parser for Go using the [Tree Sitter](http://tree-sitter.github.io/tree-sitter/) framework. It's generated by `npm run-script gen-parser`. + +It's more convenient to check the .wasm file into source than to run `gen-parser` as part of the build, because `gen-parser` requires a C++ toolchain on the host. If you want to regenerate `tree-sitter-go.wasm`, I recommend using an Ubuntu linux host. \ No newline at end of file diff --git a/parsers/tree-sitter-go.wasm b/parsers/tree-sitter-go.wasm new file mode 100644 index 000000000..d73dbec79 Binary files /dev/null and b/parsers/tree-sitter-go.wasm differ diff --git a/src/goMain.ts b/src/goMain.ts index 32260229d..2e6075460 100644 --- a/src/goMain.ts +++ b/src/goMain.ts @@ -35,6 +35,9 @@ import { buildCode } from './goBuild'; import { installCurrentPackage } from './goInstall'; import { check, removeTestStatus, notifyIfGeneratedFile } from './goCheck'; import { GO111MODULE } from './goModules'; +import { activate as activateTreeSitter } from 'vscode-tree-sitter'; +import { color } from './treeSitter'; +import * as path from 'path'; export let buildDiagnosticCollection: vscode.DiagnosticCollection; export let lintDiagnosticCollection: vscode.DiagnosticCollection; @@ -359,6 +362,32 @@ export function activate(ctx: vscode.ExtensionContext): void { }); sendTelemetryEventForConfig(vscode.workspace.getConfiguration('go', vscode.window.activeTextEditor ? vscode.window.activeTextEditor.document.uri : null)); + // Parse .go files incrementally using tree-sitter + const parserPath = path.join(ctx.extensionPath, 'parsers', 'tree-sitter-go.wasm'); + activateTreeSitter(ctx, {'go': {wasm: parserPath}}).then(colorAll); + function colorAll() { + for (const editor of vscode.window.visibleTextEditors) { + color(editor); + } + } + function colorEdited(evt: vscode.TextDocumentChangeEvent) { + for (const editor of vscode.window.visibleTextEditors) { + if (editor.document.uri.toString() === evt.document.uri.toString()) { + color(editor); + } + } + } + function onChangeConfiguration(event: vscode.ConfigurationChangeEvent) { + const colorizationNeedsReload: boolean = event.affectsConfiguration('workbench.colorTheme') + || event.affectsConfiguration('editor.tokenColorCustomizations'); + if (colorizationNeedsReload) { + colorAll(); + } + } + ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration(onChangeConfiguration)); + ctx.subscriptions.push(vscode.window.onDidChangeVisibleTextEditors(colorAll)); + ctx.subscriptions.push(vscode.window.onDidChangeTextEditorVisibleRanges(change => color(change.textEditor))); + ctx.subscriptions.push(vscode.workspace.onDidChangeTextDocument(colorEdited)); } export function deactivate() { diff --git a/src/treeSitter.ts b/src/treeSitter.ts new file mode 100644 index 000000000..8b2141c47 --- /dev/null +++ b/src/treeSitter.ts @@ -0,0 +1,32 @@ +import vscode = require('vscode'); +import {tree, decoration} from 'vscode-tree-sitter'; +import {colorGo, Range} from './treeSitterColor'; + +export function color(editor: vscode.TextEditor) { + try { + if (editor.document.languageId !== 'go') return; + const visibleRanges = editor.visibleRanges.map(range => { + const start = range.start.line; + const end = range.end.line; + return {start, end}; + }); + const t = tree(editor.document.uri); + if (t == null) { + console.warn(editor.document.uri.path, 'has not been parsed'); + return; + } + const colors = colorGo(t, visibleRanges); + for (const scope of Object.keys(colors)) { + const dec = decoration(scope); + if (!dec) continue; + const ranges = colors[scope]!.map(range); + editor.setDecorations(dec, ranges); + } + } catch (e) { + console.error(e); + } +} + +function range(x: Range): vscode.Range { + return new vscode.Range(x.start.row, x.start.column, x.end.row, x.end.column); +} diff --git a/src/treeSitterColor.ts b/src/treeSitterColor.ts new file mode 100644 index 000000000..f184eb7db --- /dev/null +++ b/src/treeSitterColor.ts @@ -0,0 +1,279 @@ +import * as Parser from 'web-tree-sitter'; + +export interface Range { + start: Parser.Point; + end: Parser.Point; +} + +export function colorGo(root: Parser.Tree, visibleRanges: {start: number, end: number}[]): {[scope: string]: Range[]} { + const functions: Range[] = []; + const types: Range[] = []; + const variables: Range[] = []; + const underlines: Range[] = []; + // Guess package names based on paths + const packages: {[id: string]: boolean} = {}; + function scanImport(x: Parser.SyntaxNode) { + if (x.type === 'import_spec') { + let str = x.firstChild!.text; + if (str.startsWith('"')) { + str = str.substring(1, str.length - 1); + } + const parts = str.split('/'); + const last = parts[parts.length - 1]; + packages[last] = true; + } + for (const child of x.children) { + scanImport(child); + } + } + // Keep track of local vars that shadow packages + const allScopes: Scope[] = []; + class Scope { + private locals = new Map(); + private parent: Scope|null; + + constructor(parent: Scope|null) { + this.parent = parent; + allScopes.push(this); + } + + declareLocal(id: string) { + if (this.isRoot()) return; + if (this.locals.has(id)) { + this.locals.get(id)!.modified = true; + } else { + this.locals.set(id, {modified: false, references: []}); + } + } + + modifyLocal(id: string) { + if (this.isRoot()) return; + if (this.locals.has(id)) this.locals.get(id)!.modified = true; + else if (this.parent) this.parent.modifyLocal(id); + } + + referenceLocal(x: Parser.SyntaxNode) { + if (this.isRoot()) return; + const id = x.text; + if (this.locals.has(id)) this.locals.get(id)!.references.push(x); + else if (this.parent) this.parent.referenceLocal(x); + } + + isLocal(id: string): boolean { + if (this.locals.has(id)) return true; + if (this.parent) return this.parent.isLocal(id); + return false; + } + + isUnknown(id: string): boolean { + if (packages[id]) return false; + if (this.locals.has(id)) return false; + if (this.parent) return this.parent.isUnknown(id); + return true; + } + + isModified(id: string): boolean { + if (this.locals.has(id)) return this.locals.get(id)!.modified; + if (this.parent) return this.parent.isModified(id); + return false; + } + + modifiedLocals(): Parser.SyntaxNode[] { + const all = []; + for (const {modified, references} of this.locals.values()) { + if (modified) { + all.push(...references); + } + } + return all; + } + + isPackage(id: string): boolean { + return packages[id] && !this.isLocal(id); + } + + isRoot(): boolean { + return this.parent === null; + } + } + const rootScope = new Scope(null); + function scanSourceFile() { + for (const top of root.rootNode.namedChildren) { + scanTopLevelDeclaration(top); + } + } + function scanTopLevelDeclaration(x: Parser.SyntaxNode) { + switch (x.type) { + case 'import_declaration': + scanImport(x); + break; + case 'function_declaration': + case 'method_declaration': + if (!isVisible(x, visibleRanges)) return; + scanFunctionDeclaration(x); + break; + case 'const_declaration': + case 'var_declaration': + if (!isVisible(x, visibleRanges)) return; + scanVarDeclaration(x); + break; + case 'type_declaration': + if (!isVisible(x, visibleRanges)) return; + scanTypeDeclaration(x); + break; + } + } + function scanFunctionDeclaration(x: Parser.SyntaxNode) { + const scope = new Scope(rootScope); + for (const child of x.namedChildren) { + switch (child.type) { + case 'identifier': + if (isVisible(child, visibleRanges)) { + functions.push({start: child.startPosition, end: child.endPosition}); + } + break; + default: + scanExpr(child, scope); + } + } + } + function scanVarDeclaration(x: Parser.SyntaxNode) { + for (const varSpec of x.namedChildren) { + for (const child of varSpec.namedChildren) { + switch (child.type) { + case 'identifier': + if (isVisible(child, visibleRanges)) { + variables.push({start: child.startPosition, end: child.endPosition}); + } + break; + default: + scanExpr(child, rootScope); + } + } + } + } + function scanTypeDeclaration(x: Parser.SyntaxNode) { + for (const child of x.namedChildren) { + scanExpr(child, rootScope); + } + } + function scanExpr(x: Parser.SyntaxNode, scope: Scope) { + switch (x.type) { + case 'ERROR': + return; + case 'func_literal': + case 'block': + case 'expression_case_clause': + case 'type_case_clause': + case 'for_statement': + case 'if_statement': + case 'type_switch_statement': + scope = new Scope(scope); + break; + case 'parameter_declaration': + case 'variadic_parameter_declaration': + case 'var_spec': + case 'const_spec': + for (const id of x.namedChildren) { + if (id.type === 'identifier') { + scope.declareLocal(id.text); + } + } + break; + case 'short_var_declaration': + case 'range_clause': + for (const id of x.firstChild!.namedChildren) { + if (id.type === 'identifier') { + scope.declareLocal(id.text); + } + } + break; + case 'type_switch_guard': + if (x.firstChild!.type === 'expression_list') { + for (const id of x.firstChild!.namedChildren) { + scope.declareLocal(id.text); + } + } + break; + case 'inc_statement': + case 'dec_statement': + scope.modifyLocal(x.firstChild!.text); + break; + case 'assignment_statement': + for (const id of x.firstChild!.namedChildren) { + if (id.type === 'identifier') { + scope.modifyLocal(id.text); + } + } + break; + case 'call_expression': + scanCall(x.firstChild!, scope); + scanExpr(x.lastChild!, scope); + return; + case 'identifier': + scope.referenceLocal(x); + if (isVisible(x, visibleRanges) && scope.isUnknown(x.text)) { + variables.push({start: x.startPosition, end: x.endPosition}); + } + return; + case 'selector_expression': + if (isVisible(x, visibleRanges) && scope.isPackage(x.firstChild!.text)) { + variables.push({start: x.lastChild!.startPosition, end: x.lastChild!.endPosition}); + } + scanExpr(x.firstChild!, scope); + scanExpr(x.lastChild!, scope); + return; + case 'type_identifier': + if (isVisible(x, visibleRanges)) { + types.push({start: x.startPosition, end: x.endPosition}); + } + return; + } + for (const child of x.namedChildren) { + scanExpr(child, scope); + } + } + function scanCall(x: Parser.SyntaxNode, scope: Scope) { + switch (x.type) { + case 'identifier': + if (isVisible(x, visibleRanges) && scope.isUnknown(x.text)) { + functions.push({start: x.startPosition, end: x.endPosition}); + } + scope.referenceLocal(x); + return; + case 'selector_expression': + if (isVisible(x, visibleRanges) && scope.isPackage(x.firstChild!.text)) { + functions.push({start: x.lastChild!.startPosition, end: x.lastChild!.endPosition}); + } + scanExpr(x.firstChild!, scope); + scanExpr(x.lastChild!, scope); + return; + case 'unary_expression': + scanCall(x.firstChild!, scope); + return; + default: + scanExpr(x, scope); + } + } + scanSourceFile(); + for (const scope of allScopes) { + for (const local of scope.modifiedLocals()) { + underlines.push({start: local.startPosition, end: local.endPosition}); + } + } + + return { + 'entity.name.function': functions, + 'entity.name.type': types, + 'variable': variables, + 'markup.underline': underlines, + }; +} + +function isVisible(x: Parser.SyntaxNode, visibleRanges: {start: number, end: number}[]) { + for (const {start, end} of visibleRanges) { + const overlap = x.startPosition.row <= end + 1 && start - 1 <= x.endPosition.row; + if (overlap) return true; + } + return false; +} diff --git a/syntaxes/go.tmGrammar.json b/syntaxes/go.tmGrammar.json new file mode 100644 index 000000000..10aebe19b --- /dev/null +++ b/syntaxes/go.tmGrammar.json @@ -0,0 +1,252 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "version": "https://github.com/atom/language-go/commit/b6fd68f74efa109679e31fe6f4a41ac105262d0e", + "name": "Go", + "scopeName": "source.go", + "comment": "Go language", + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "Interpreted string literals", + "begin": "\"", + "end": "\"", + "name": "string.quoted.double.go", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_placeholder" + } + ] + }, + { + "comment": "Raw string literals", + "begin": "`", + "end": "`", + "name": "string.quoted.raw.go", + "patterns": [ + { + "include": "#string_placeholder" + } + ] + }, + { + "comment": "Syntax error receiving channels", + "match": "<\\-([\\t ]+)chan\\b", + "captures": { + "1": { + "name": "invalid.illegal.receive-channel.go" + } + } + }, + { + "comment": "Syntax error sending channels", + "match": "\\bchan([\\t ]+)<-", + "captures": { + "1": { + "name": "invalid.illegal.send-channel.go" + } + } + }, + { + "comment": "Syntax error using slices", + "match": "\\[\\](\\s+)", + "captures": { + "1": { + "name": "invalid.illegal.slice.go" + } + } + }, + { + "comment": "Floating-point literals", + "match": "(\\.\\d+([Ee][-+]\\d+)?i?)\\b|\\b\\d+\\.\\d*(([Ee][-+]\\d+)?i?\\b)?", + "name": "constant.numeric.floating-point.go" + }, + { + "comment": "Integers", + "match": "\\b((0x[0-9a-fA-F]+)|(0[0-7]+i?)|(\\d+([Ee]\\d+)?i?)|(\\d+[Ee][-+]\\d+i?))\\b", + "name": "constant.numeric.integer.go" + }, + { + "comment": "Language constants", + "match": "\\b(true|false|nil|iota)\\b", + "name": "constant.numeric.language.go" + }, + { + "comment": "Terminators", + "match": ";", + "name": "keyword.other.semi.go" + }, + { + "include": "#keywords" + }, + { + "include": "#operators" + }, + { + "include": "#runes" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "begin": "/\\*", + "end": "\\*/", + "name": "comment.block.go" + }, + { + "begin": "//", + "end": "$", + "name": "comment.line.double-slash.go" + } + ] + }, + "keywords": { + "patterns": [ + { + "comment": "Flow control keywords", + "match": "\\b(break|case|continue|default|defer|panic|recover|else|fallthrough|for|go|goto|if|range|return|select|switch)\\b", + "name": "keyword.control.go" + }, + { + "match": "\\bchan\\b", + "name": "keyword.channel.go" + }, + { + "match": "\\bconst\\b", + "name": "keyword.const.go" + }, + { + "match": "\\bfunc\\b", + "name": "keyword.function.go" + }, + { + "match": "\\binterface\\b", + "name": "keyword.interface.go" + }, + { + "match": "\\bmap\\b", + "name": "keyword.map.go" + }, + { + "match": "\\bstruct\\b", + "name": "keyword.struct.go" + }, + { + "comment": "Syntax error numeric literals", + "match": "\\b0[0-7]*[89]\\d*\\b", + "name": "invalid.illegal.numeric.go" + }, + { + "comment": "Functions", + "match": "\\bfunc\\b", + "name": "keyword.function.go" + }, + { + "match": "\\bpackage\\b", + "name": "keyword.package.go" + }, + { + "match": "\\btype\\b", + "name": "keyword.type.go" + }, + { + "match": "\\bimport\\b", + "name": "keyword.import.go" + }, + { + "match": "\\bvar\\b", + "name": "keyword.var.go" + } + ] + }, + "operators": { + "comment": "Note that the order here is very important!", + "patterns": [ + { + "match": "(\\*|&)(?=\\w)", + "name": "keyword.operator.address.go" + }, + { + "match": "<\\-", + "name": "keyword.operator.channel.go" + }, + { + "match": "\\-\\-", + "name": "keyword.operator.decrement.go" + }, + { + "match": "\\+\\+", + "name": "keyword.operator.increment.go" + }, + { + "match": "(==|!=|<=|>=|<(?!<)|>(?!>))", + "name": "keyword.operator.comparison.go" + }, + { + "match": "(&&|\\|\\||!)", + "name": "keyword.operator.logical.go" + }, + { + "match": "(=|\\+=|\\-=|\\|=|\\^=|\\*=|/=|:=|%=|<<=|>>=|&\\^=|&=)", + "name": "keyword.operator.assignment.go" + }, + { + "match": "(\\+|\\-|\\*|/|%)", + "name": "keyword.operator.arithmetic.go" + }, + { + "match": "(&(?!\\^)|\\||\\^|&\\^|<<|>>)", + "name": "keyword.operator.arithmetic.bitwise.go" + }, + { + "match": "\\.\\.\\.", + "name": "keyword.operator.ellipsis.go" + } + ] + }, + "runes": { + "patterns": [ + { + "begin": "'", + "end": "'", + "name": "string.quoted.rune.go", + "patterns": [ + { + "match": "\\G(\\\\([0-7]{3}|[abfnrtv\\\\'\"]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|.)(?=')", + "name": "constant.other.rune.go" + }, + { + "match": "[^']+", + "name": "invalid.illegal.unknown-rune.go" + } + ] + } + ] + }, + "string_escaped_char": { + "patterns": [ + { + "match": "\\\\([0-7]{3}|[abfnrtv\\\\'\"]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})", + "name": "constant.character.escape.go" + }, + { + "match": "\\\\[^0-7xuUabfnrtv\\'\"]", + "name": "invalid.illegal.unknown-escape.go" + } + ] + }, + "string_placeholder": { + "patterns": [ + { + "match": "%(\\[\\d+\\])?([\\+#\\-0\\x20]{,2}((\\d+|\\*)?(\\.?(\\d+|\\*|(\\[\\d+\\])\\*?)?(\\[\\d+\\])?)?))?[vT%tbcdoqxXUbeEfFgGsp]", + "name": "constant.other.placeholder.go" + } + ] + } + } +} \ No newline at end of file diff --git a/test/unit/treeSitter.test.ts b/test/unit/treeSitter.test.ts new file mode 100644 index 000000000..a4af605d9 --- /dev/null +++ b/test/unit/treeSitter.test.ts @@ -0,0 +1,226 @@ +import Parser = require('web-tree-sitter'); +import { colorGo } from '../../src/treeSitterColor'; +import * as assert from 'assert'; + +type check = [string, string | { not: string }]; +type TestCase = [string, ...check[]]; + +const testCases: TestCase[] = [ + [ + `func f() int { }`, + ['f', 'entity.name.function'], ['int', 'entity.name.type'] + ], + [ + `type Foo struct { x int }`, + ['Foo', 'entity.name.type'], ['x', { not: 'variable' }] + ], + [ + `type Foo interface { GetX() int }`, + ['Foo', 'entity.name.type'], ['int', 'entity.name.type'], ['GetX', { not: 'variable' }] + ], + [ + `func f() { x := 1; x := 2 }`, + ['x', 'markup.underline'] + ], + [ + `func f(foo T) { foo.Foo() }`, + ['Foo', { not: 'entity.name.function' }] + ], + [ + `func f() { Foo() }`, + ['Foo', 'entity.name.function'] + ], + [ + `import "foo"; func f() { foo.Foo() }`, + ['Foo', 'entity.name.function'] + ], + [ + `import "foo"; func f(foo T) { foo.Foo() }`, + ['Foo', { not: 'entity.name.function' }] + ], + [ + `func f(x other.T) { }`, + ['T', 'entity.name.type'], + ], + [ + `var _ = f(Foo{})`, + ['Foo', 'entity.name.type'], + ], + [ + `import (foo "foobar"); var _ = foo.Bar()`, + ['foo', { not: 'variable' }], ['Bar', 'entity.name.function'], + ], + [ + `func f(a int) int { + switch a { + case 1: + x := 1 + return x + case 2: + x := 2 + return x + } + }`, + ['x', { not: 'markup.underline' }] + ], + [ + `func f(a interface{}) int { + switch a.(type) { + case *int: + x := 1 + return x + case *int: + x := 2 + return x + } + }`, + ['x', { not: 'markup.underline' }] + ], + [ + `func f(a interface{}) int { + for i := range 10 { + print(i) + } + for i := range 10 { + print(i) + } + }`, + ['i', { not: 'markup.underline' }] + ], + [ + `func f(a interface{}) int { + if i := 1; i < 10 { + print(i) + } + if i := 1; i < 10 { + print(i) + } + }`, + ['i', { not: 'markup.underline' }] + ], + [ + `func f(a interface{}) { + switch aa := a.(type) { + case *int: + print(aa) + } + }`, + ['aa', { not: 'variable' }] + ], + [ + `func f() { + switch aa.(type) { + case *int: + print(aa) + } + }`, + ['aa', 'variable'] + ], + [ + `func f(a interface{}) { + switch aa := a.(type) { + case *int: + print(aa) + } + switch aa := a.(type) { + case *int: + print(aa) + } + }`, + ['aa', { not: 'markup.underline' }] + ], + [ + `func f(a ...int) { + print(a) + }`, + ['a', { not: 'variable' }] + ], +]; + +async function createParser() { + await Parser.init(); + const parser = new Parser(); + const wasm = 'parsers/tree-sitter-go.wasm'; + const lang = await Parser.Language.load(wasm); + parser.setLanguage(lang); + return parser; +} + +suite('Syntax coloring', () => { + const parser = createParser(); + + for (const [src, ...expect] of testCases) { + test(src, async () => { + const tree = (await parser).parse(src); + const scope2ranges = colorGo(tree, [{start: 0, end: tree.rootNode.endPosition.row}]); + const code2scopes = new Map>(); + for (const scope of Object.keys(scope2ranges)) { + for (const range of scope2ranges[scope]) { + const start = index(src, range.start); + const end = index(src, range.end); + const code = src.substring(start, end); + if (!code2scopes.has(code)) { + code2scopes.set(code, new Set()); + } + code2scopes.get(code)!.add(scope); + } + } + function printSrcAndTree() { + console.error('Source:\t' + src); + console.error('Parsed:\t' + tree.rootNode.toString()); + } + for (const [code, check] of expect) { + if (typeof check === 'string') { + const scope = check; + if (!code2scopes.has(code)) { + printSrcAndTree(); + assert.fail(`Error:\tcode (${code}) was not found in (${join(code2scopes.keys())})`); + continue; + } + const foundScopes = code2scopes.get(code)!; + if (!foundScopes.has(scope)) { + printSrcAndTree(); + assert.fail(`Error:\tscope (${scope}) was not among the scopes for (${code}) (${join(foundScopes.keys())})`); + continue; + } + } else { + const scope = check.not; + if (!code2scopes.has(code)) { + continue; + } + const foundScopes = code2scopes.get(code)!; + if (foundScopes.has(scope)) { + printSrcAndTree(); + assert.fail(`Error:\tbanned scope (${scope}) was among the scopes for (${code}) (${join(foundScopes.keys())})`); + continue; + } + } + } + }); + } +}); + +function index(code: string, point: Parser.Point): number { + let row = 0; + let column = 0; + for (let i = 0; i < code.length; i++) { + if (row === point.row && column === point.column) { + return i; + } + if (code[i] === '\n') { + row++; + column = 0; + } else { + column++; + } + } + return code.length; +} + +function join(strings: IterableIterator) { + let result = ''; + for (const s of strings) { + result = result + s + ', '; + } + return result.substring(0, result.length - 2); +}