├── .gitignore ├── .vscodeignore ├── tsconfig.json ├── .gitattributes ├── README.md ├── .vscode ├── tasks.json ├── launch.json └── tasks.json.old ├── test ├── index.ts └── extension.test.ts ├── LICENSE ├── src ├── hscopes.d.ts ├── text-util.ts ├── document.ts └── extension.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | typings 4 | *.vsix -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.vsix 7 | **/*.map 8 | .gitignore 9 | tsconfig.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019", "DOM"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "rootDir": ".", 9 | "allowJs": false 10 | }, 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperScopes 2 | 3 | A meta-extension for vscode that provides TextMate scope information. Its 4 | intended usage is as a library for other extensions to query scope information. 5 | 6 | ## Usage 7 | 8 | This extension provides an API by which your extension can query scope & token 9 | information. Refer to `hscopes.d.ts` and `extension.test.ts` for more details. 10 | Example usage: 11 | 12 | ```ts 13 | import * as vscode from 'vscode'; 14 | 15 | async function example(doc : vscode.TextDocument, pos: vscode.Position) : void { 16 | const hs = vscode.extensions.getExtension('draivin.hscopes'); 17 | const token : scopeInfo.Token = hs.getScopeAt(doc, pos); 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | "tasks": [ 17 | { 18 | "label": "watch", 19 | "type": "shell", 20 | "args": [ 21 | "run", 22 | "watch" 23 | ], 24 | "isBackground": true, 25 | "problemMatcher": "$tsc-watch", 26 | "group": "build" 27 | } 28 | ] 29 | 30 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ 14 | "${workspaceRoot}/out/**/*.js" 15 | ], 16 | "preLaunchTask": "watch" 17 | }, 18 | { 19 | "name": "Launch Tests", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "runtimeExecutable": "${execPath}", 23 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 24 | "sourceMaps": true, 25 | "outFiles": ["${workspaceRoot}/out/test"], 26 | "preLaunchTask": "watch" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/tasks.json.old: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | "suppressTaskName": true, 20 | 21 | // show the output window only if unrecognized errors occur. 22 | "showOutput": "silent", 23 | 24 | "tasks": [ 25 | { 26 | "taskName": "watch", 27 | "args": ["run", "watch"], 28 | "isWatching": true, 29 | "isBuildCommand": true, 30 | "isTestCommand": false, 31 | "problemMatcher": "$tsc-watch" 32 | } 33 | ] 34 | 35 | } -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Portions Copyright (c) 2021 Ian Ornelas 4 | Portions Copyright (c) 2016 Christian J. Bell as part of project scope-info 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /src/hscopes.d.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * A grammar 5 | */ 6 | 7 | export interface IGrammar { 8 | /** 9 | * Tokenize `lineText` using previous line state `prevState`. 10 | */ 11 | tokenizeLine(lineText: string, prevState: StackElement | null): ITokenizeLineResult; 12 | } 13 | 14 | export interface ITokenizeLineResult { 15 | readonly tokens: IToken[]; 16 | /** 17 | * The `prevState` to be passed on to the next line tokenization. 18 | */ 19 | readonly ruleStack: StackElement; 20 | } 21 | 22 | export interface IToken { 23 | startIndex: number; 24 | readonly endIndex: number; 25 | readonly scopes: string[]; 26 | } 27 | 28 | export interface StackElement { 29 | _stackElementBrand: void; 30 | readonly depth: number; 31 | clone(): StackElement; 32 | equals(other: StackElement): boolean; 33 | } 34 | 35 | export interface Token { 36 | range: vscode.Range; 37 | text: string; 38 | scopes: string[]; 39 | } 40 | 41 | export interface HScopesAPI { 42 | reloadScope(document: vscode.TextDocument): boolean; 43 | getScopeAt(document: vscode.TextDocument, position: vscode.Position): Token | null; 44 | getGrammar(scopeName: string): Promise; 45 | getScopeForLanguage(language: string): string | null; 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hscopes", 3 | "displayName": "HyperScopes", 4 | "description": " Supplies an API for querying TextMate scope information.", 5 | "version": "0.0.9", 6 | "publisher": "draivin", 7 | "license": "MIT", 8 | "engines": { 9 | "vscode": "^1.52.0" 10 | }, 11 | "categories": [ 12 | "Other" 13 | ], 14 | "contributes": { 15 | "commands": [ 16 | { 17 | "command": "hscopes.reloadCurrentDocument", 18 | "title": "HyperScopes: Reload Scopes For Current Document" 19 | } 20 | ] 21 | }, 22 | "keywords": [ 23 | "TextMate", 24 | "scope", 25 | "grammar" 26 | ], 27 | "activationEvents": [ 28 | "onCommand:hscopes.never" 29 | ], 30 | "main": "./out/src/extension", 31 | "scripts": { 32 | "vscode:prepublish": "tsc -p ./", 33 | "watch": "tsc -watch -p ./", 34 | "compile": "tsc -p ./" 35 | }, 36 | "dependencies": { 37 | "@types/node": "^18.11.17", 38 | "vscode-oniguruma": "^1.7.0", 39 | "vscode-textmate": "^8.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/mocha": "2.2.33", 43 | "@types/vscode": "^1.52.0", 44 | "mocha": "^10.2.0", 45 | "typescript": "^4.2.4" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/draivin/hscopes/issues" 49 | }, 50 | "homepage": "https://github.com/draivin/hscopes/blob/master/README.md", 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/draivin/hscopes.git" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as api from '../src/hscopes'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite('Scope Info', function () { 16 | let si: api.HScopesAPI; 17 | test('api - getScopeForLanguage', async function () { 18 | const siExt = vscode.extensions.getExtension('draivin.hscopes'); 19 | si = await siExt.activate(); 20 | assert.strictEqual(si.getScopeForLanguage('html'), 'text.html.basic'); 21 | }); 22 | 23 | test('api - getGrammar', async function () { 24 | const siExt = vscode.extensions.getExtension('draivin.hscopes'); 25 | si = await siExt.activate(); 26 | const g = await si.getGrammar('text.html.basic'); 27 | const t = g.tokenizeLine('', null); 28 | assert.strictEqual(t.tokens.length, 11); 29 | assert.deepStrictEqual(t.tokens[0], { 30 | startIndex: 0, 31 | endIndex: 2, 32 | scopes: ['text.html.basic', 'meta.tag.sgml.html', 'punctuation.definition.tag.html'], 33 | }); 34 | assert.deepStrictEqual(t.tokens[1], { 35 | startIndex: 2, 36 | endIndex: 9, 37 | scopes: ['text.html.basic', 'meta.tag.sgml.html', 'meta.tag.sgml.doctype.html'], 38 | }); 39 | assert.deepStrictEqual(t.tokens[2], { 40 | startIndex: 9, 41 | endIndex: 14, 42 | scopes: ['text.html.basic', 'meta.tag.sgml.html', 'meta.tag.sgml.doctype.html'], 43 | }); 44 | assert.deepStrictEqual(t.tokens[7], { 45 | startIndex: 21, 46 | endIndex: 22, 47 | scopes: [ 48 | 'text.html.basic', 49 | 'meta.tag.any.html', 50 | 'punctuation.definition.tag.html', 51 | 'meta.scope.between-tag-pair.html', 52 | ], 53 | }); 54 | }); 55 | 56 | test('api - getScopeAt', async function () { 57 | const siExt = vscode.extensions.getExtension('draivin.hscopes'); 58 | si = await siExt.activate(); 59 | const file = vscode.Uri.parse('untitled:C:\test.html'); 60 | const doc = await vscode.workspace.openTextDocument(file); 61 | const ed = await vscode.window.showTextDocument(doc); 62 | await ed.edit((builder) => { 63 | builder.insert(new vscode.Position(0, 0), '\n'); 64 | }); 65 | const t1 = si.getScopeAt(doc, new vscode.Position(0, 2)); 66 | assert.strictEqual(t1.text, 'DOCTYPE'); 67 | assert.deepStrictEqual(t1.range, new vscode.Range(0, 2, 0, 9)); 68 | assert.deepStrictEqual(t1.scopes, [ 69 | 'text.html.basic', 70 | 'meta.tag.sgml.html', 71 | 'meta.tag.sgml.doctype.html', 72 | ]); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/text-util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | // 'sticky' flag is not yet supported :( 4 | const lineEndingRE = /([^\r\n]*)(\r\n|\r|\n)?/; 5 | 6 | export interface RangeDelta { 7 | start: vscode.Position; 8 | end: vscode.Position; 9 | linesDelta: number; 10 | endCharactersDelta: number; // delta for positions on the same line as the end position 11 | } 12 | 13 | /** 14 | * @returns the Position (line, column) for the location (character position) 15 | */ 16 | function positionAt(text: string, offset: number): vscode.Position { 17 | if (offset > text.length) offset = text.length; 18 | let line = 0; 19 | let lastIndex = 0; 20 | while (true) { 21 | const match = lineEndingRE.exec(text.substring(lastIndex)); 22 | if (lastIndex + match[1].length >= offset) return new vscode.Position(line, offset - lastIndex); 23 | lastIndex += match[0].length; 24 | ++line; 25 | } 26 | } 27 | 28 | /** 29 | * @returns the lines and characters represented by the text 30 | */ 31 | export function toRangeDelta(oldRange: vscode.Range, text: string): RangeDelta { 32 | const newEnd = positionAt(text, text.length); 33 | let charsDelta; 34 | if (oldRange.start.line == oldRange.end.line) 35 | charsDelta = newEnd.character - (oldRange.end.character - oldRange.start.character); 36 | else charsDelta = newEnd.character - oldRange.end.character; 37 | 38 | return { 39 | start: oldRange.start, 40 | end: oldRange.end, 41 | linesDelta: newEnd.line - (oldRange.end.line - oldRange.start.line), 42 | endCharactersDelta: charsDelta, 43 | }; 44 | } 45 | 46 | export function rangeDeltaNewRange(delta: RangeDelta): vscode.Range { 47 | let x: number; 48 | if (delta.linesDelta > 0) x = delta.endCharactersDelta; 49 | else if (delta.linesDelta < 0 && delta.start.line == delta.end.line + delta.linesDelta) 50 | x = delta.end.character + delta.endCharactersDelta + delta.start.character; 51 | else x = delta.end.character + delta.endCharactersDelta; 52 | return new vscode.Range(delta.start, new vscode.Position(delta.end.line + delta.linesDelta, x)); 53 | } 54 | 55 | function positionRangeDeltaTranslate(pos: vscode.Position, delta: RangeDelta): vscode.Position { 56 | if (pos.isBefore(delta.end)) return pos; 57 | else if (delta.end.line == pos.line) { 58 | let x = pos.character + delta.endCharactersDelta; 59 | if (delta.linesDelta > 0) x = x - delta.end.character; 60 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 61 | x = x + delta.start.character; 62 | return new vscode.Position(pos.line + delta.linesDelta, x); 63 | } // if(pos.line > delta.end.line) 64 | else return new vscode.Position(pos.line + delta.linesDelta, pos.character); 65 | } 66 | 67 | function positionRangeDeltaTranslateEnd(pos: vscode.Position, delta: RangeDelta): vscode.Position { 68 | if (pos.isBeforeOrEqual(delta.end)) return pos; 69 | else if (delta.end.line == pos.line) { 70 | let x = pos.character + delta.endCharactersDelta; 71 | if (delta.linesDelta > 0) x = x - delta.end.character; 72 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 73 | x = x + delta.start.character; 74 | return new vscode.Position(pos.line + delta.linesDelta, x); 75 | } // if(pos.line > delta.end.line) 76 | else return new vscode.Position(pos.line + delta.linesDelta, pos.character); 77 | } 78 | 79 | export function rangeTranslate(range: vscode.Range, delta: RangeDelta) { 80 | return new vscode.Range( 81 | positionRangeDeltaTranslate(range.start, delta), 82 | positionRangeDeltaTranslateEnd(range.end, delta) 83 | ); 84 | } 85 | 86 | export function rangeContains( 87 | range: vscode.Range, 88 | pos: vscode.Position, 89 | exclStart = false, 90 | inclEnd = false 91 | ) { 92 | return ( 93 | range.start.isBeforeOrEqual(pos) && 94 | (!exclStart || !range.start.isEqual(pos)) && 95 | ((inclEnd && range.end.isEqual(pos)) || range.end.isAfter(pos)) 96 | ); 97 | } 98 | 99 | export function maxPosition(x: vscode.Position, y: vscode.Position) { 100 | if (x.line < y.line) return x; 101 | if (x.line < x.line) return y; 102 | if (x.character < y.character) return x; 103 | else return y; 104 | } 105 | -------------------------------------------------------------------------------- /src/document.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as textUtil from './text-util'; 3 | import * as api from './hscopes'; 4 | import * as tm from 'vscode-textmate'; 5 | 6 | export class DocumentController implements vscode.Disposable { 7 | private subscriptions: vscode.Disposable[] = []; 8 | 9 | // Stores the state for each line 10 | private grammarState: tm.StateStack[] = []; 11 | private grammar: tm.IGrammar; 12 | 13 | public constructor( 14 | doc: vscode.TextDocument, 15 | textMateGrammar: tm.IGrammar, 16 | private document = doc 17 | ) { 18 | this.grammar = textMateGrammar; 19 | 20 | // Parse whole document 21 | const docRange = new vscode.Range(0, 0, this.document.lineCount, 0); 22 | this.reparsePretties(docRange); 23 | 24 | this.subscriptions.push( 25 | vscode.workspace.onDidChangeTextDocument((e) => { 26 | if (e.document == this.document) this.onChangeDocument(e); 27 | }) 28 | ); 29 | } 30 | 31 | public dispose() { 32 | this.subscriptions.forEach((s) => s.dispose()); 33 | } 34 | 35 | private refreshTokensOnLine(line: vscode.TextLine): { 36 | tokens: tm.IToken[]; 37 | invalidated: boolean; 38 | } { 39 | if (!this.grammar) return { tokens: [], invalidated: false }; 40 | const prevState = this.grammarState[line.lineNumber - 1] || null; 41 | const lineTokens = this.grammar.tokenizeLine(line.text, prevState); 42 | const invalidated = 43 | !this.grammarState[line.lineNumber] || 44 | !lineTokens.ruleStack.equals(this.grammarState[line.lineNumber]); 45 | this.grammarState[line.lineNumber] = lineTokens.ruleStack; 46 | return { tokens: lineTokens.tokens, invalidated: invalidated }; 47 | } 48 | 49 | public getScopeAt(position: vscode.Position): api.Token | null { 50 | if (!this.grammar) return null; 51 | position = this.document.validatePosition(position); 52 | const state = this.grammarState[position.line - 1] || null; 53 | const line = this.document.lineAt(position.line); 54 | const tokens = this.grammar.tokenizeLine(line.text, state); 55 | for (let t of tokens.tokens) { 56 | if (t.startIndex <= position.character && position.character < t.endIndex) 57 | return { 58 | range: new vscode.Range(position.line, t.startIndex, position.line, t.endIndex), 59 | text: line.text.substring(t.startIndex, t.endIndex), 60 | scopes: t.scopes, 61 | }; 62 | } 63 | // FIXME: No token matched, return last token in the line. 64 | let lastToken = tokens.tokens[tokens.tokens.length - 1]; 65 | return { 66 | range: new vscode.Range( 67 | position.line, 68 | lastToken.startIndex, 69 | position.line, 70 | lastToken.endIndex 71 | ), 72 | text: line.text.substring(lastToken.startIndex, lastToken.endIndex), 73 | scopes: lastToken.scopes, 74 | }; 75 | } 76 | 77 | private reparsePretties(range: vscode.Range): void { 78 | range = this.document.validateRange(range); 79 | 80 | let invalidatedTokenState = false; 81 | 82 | // Collect new pretties 83 | const lineCount = this.document.lineCount; 84 | let lineIdx: number; 85 | for ( 86 | lineIdx = range.start.line; 87 | lineIdx <= range.end.line || (invalidatedTokenState && lineIdx < lineCount); 88 | ++lineIdx 89 | ) { 90 | const line = this.document.lineAt(lineIdx); 91 | const { invalidated: invalidated } = this.refreshTokensOnLine(line); 92 | invalidatedTokenState = invalidated; 93 | } 94 | } 95 | 96 | private applyChanges(changes: readonly vscode.TextDocumentContentChangeEvent[]) { 97 | const sortedChanges = [...changes].sort((change1, change2) => 98 | change1.range.start.isAfter(change2.range.start) ? -1 : 1 99 | ); 100 | for (const change of sortedChanges) { 101 | try { 102 | const delta = textUtil.toRangeDelta(change.range, change.text); 103 | 104 | if (delta.linesDelta < 0) { 105 | let deleteStart = delta.start.line + 1 + delta.linesDelta; 106 | if (deleteStart < 0) { 107 | return this.refresh(); 108 | } 109 | this.grammarState.splice(deleteStart, -delta.linesDelta); 110 | } else if (delta.linesDelta > 0) { 111 | this.grammarState.splice( 112 | delta.start.line, 113 | 0, 114 | ...Array(delta.linesDelta).fill(tm.INITIAL) 115 | ); 116 | } 117 | 118 | if (this.grammarState.length != this.document.lineCount) { 119 | return this.refresh(); 120 | } 121 | 122 | const editRange = textUtil.rangeDeltaNewRange(delta); 123 | this.reparsePretties(editRange); 124 | } catch (e) { 125 | console.error(e); 126 | } 127 | } 128 | } 129 | 130 | private onChangeDocument(event: vscode.TextDocumentChangeEvent) { 131 | this.applyChanges(event.contentChanges); 132 | } 133 | 134 | public refresh() { 135 | this.grammarState = []; 136 | const docRange = new vscode.Range(0, 0, this.document.lineCount, 0); 137 | this.reparsePretties(docRange); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | import * as tm from 'vscode-textmate'; 5 | import * as oniguruma from 'vscode-oniguruma'; 6 | import * as path from 'path'; 7 | import * as fs from 'fs'; 8 | import { DocumentController } from './document'; 9 | import * as api from './hscopes'; 10 | 11 | const wasmBin = fs.readFileSync( 12 | path.join(__dirname, '../../node_modules/vscode-oniguruma/release/onig.wasm') 13 | ).buffer; 14 | const vscodeOnigurumaLib = oniguruma.loadWASM(wasmBin).then(() => { 15 | return { 16 | createOnigScanner: (sources: any) => oniguruma.createOnigScanner(sources), 17 | createOnigString: (s: any) => oniguruma.createOnigString(s), 18 | }; 19 | }); 20 | 21 | /** Tracks all documents that substitutions are being applied to */ 22 | let documents = new Map(); 23 | 24 | export let registry: tm.Registry; 25 | 26 | interface ExtensionGrammar { 27 | language?: string; 28 | scopeName?: string; 29 | path?: string; 30 | embeddedLanguages?: { [scopeName: string]: string }; 31 | injectTo?: string[]; 32 | } 33 | interface ExtensionPackage { 34 | contributes?: { 35 | languages?: { id: string; configuration: string }[]; 36 | grammars?: ExtensionGrammar[]; 37 | }; 38 | } 39 | 40 | function getLanguageScopeName(languageId: string): string { 41 | try { 42 | const languages = vscode.extensions.all 43 | .filter( 44 | (x) => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars 45 | ) 46 | .reduce( 47 | (a: ExtensionGrammar[], b) => [ 48 | ...a, 49 | ...(b.packageJSON as ExtensionPackage).contributes.grammars, 50 | ], 51 | [] 52 | ); 53 | const matchingLanguages = languages.filter((g) => g.language === languageId); 54 | 55 | if (matchingLanguages.length > 0) { 56 | // console.info(`Mapping language ${languageId} to initial scope ${matchingLanguages[0].scopeName}`); 57 | return matchingLanguages[0].scopeName; 58 | } 59 | } catch (err) {} 60 | return undefined; 61 | } 62 | 63 | export let workspaceState: vscode.Memento; 64 | 65 | /** initialize everything; main entry point */ 66 | export function activate(context: vscode.ExtensionContext): api.HScopesAPI { 67 | workspaceState = context.workspaceState; 68 | 69 | context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(openDocument)); 70 | context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(closeDocument)); 71 | 72 | reloadGrammar(); 73 | 74 | const api: api.HScopesAPI = { 75 | reloadScope(document: vscode.TextDocument): boolean { 76 | const prettyDoc = documents.get(document.uri); 77 | if (prettyDoc) { 78 | prettyDoc.refresh(); 79 | return true; 80 | } 81 | return false; 82 | }, 83 | getScopeAt(document: vscode.TextDocument, position: vscode.Position): api.Token | null { 84 | try { 85 | const prettyDoc = documents.get(document.uri); 86 | if (prettyDoc) { 87 | return prettyDoc.getScopeAt(position); 88 | } 89 | } catch (err) {} 90 | return null; 91 | }, 92 | getScopeForLanguage(language: string): string | null { 93 | return getLanguageScopeName(language) || null; 94 | }, 95 | async getGrammar(scopeName: string): Promise { 96 | try { 97 | if (registry) return await registry.loadGrammar(scopeName); 98 | } catch (err) {} 99 | return null; 100 | }, 101 | }; 102 | 103 | context.subscriptions.push( 104 | vscode.commands.registerTextEditorCommand('hscopes.reloadCurrentDocument', (editor) => { 105 | api.reloadScope(editor.document); 106 | }) 107 | ); 108 | 109 | return api; 110 | } 111 | 112 | /** Re-read the settings and recreate substitutions for all documents */ 113 | function reloadGrammar() { 114 | try { 115 | registry = new tm.Registry({ 116 | onigLib: vscodeOnigurumaLib, 117 | getInjections: (scopeName) => { 118 | let extensions = vscode.extensions.all.filter( 119 | (x) => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars 120 | ); 121 | 122 | let grammars = extensions.flatMap((e) => { 123 | return (e.packageJSON as ExtensionPackage).contributes!.grammars; 124 | }); 125 | 126 | return grammars 127 | .filter((g) => g.injectTo && g.injectTo.some((s) => s === scopeName)) 128 | .map((g) => g.scopeName); 129 | }, 130 | 131 | loadGrammar: async (scopeName) => { 132 | try { 133 | let extensions = vscode.extensions.all.filter( 134 | (x) => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars 135 | ); 136 | 137 | let grammars = extensions.flatMap((e) => { 138 | return (e.packageJSON as ExtensionPackage).contributes!.grammars.map((g) => { 139 | return { extensionPath: e.extensionPath, ...g }; 140 | }); 141 | }); 142 | 143 | const matchingGrammars = grammars.filter((g) => g.scopeName === scopeName); 144 | 145 | if (matchingGrammars.length > 0) { 146 | const grammar = matchingGrammars[0]; 147 | const filePath = path.join(grammar.extensionPath, grammar.path); 148 | let content = await fs.promises.readFile(filePath, 'utf-8'); 149 | return await tm.parseRawGrammar(content, filePath); 150 | } 151 | } catch (err) { 152 | console.error(`HyperScopes: Unable to load grammar for scope ${scopeName}.`, err); 153 | } 154 | return undefined; 155 | }, 156 | }); 157 | } catch (err) { 158 | registry = undefined; 159 | console.error(err); 160 | } 161 | 162 | // Recreate the documents 163 | unloadDocuments(); 164 | for (const doc of vscode.workspace.textDocuments) openDocument(doc); 165 | } 166 | 167 | const blacklist = [ 168 | '\\settings', 169 | '\\ignoredSettings', 170 | '\\launch', 171 | '\\token-styling', 172 | '\\textmate-colors', 173 | '\\workbench-colors', 174 | ]; 175 | 176 | async function openDocument(doc: vscode.TextDocument) { 177 | for (let entry of blacklist) { 178 | if (doc.fileName.startsWith(entry)) return; 179 | } 180 | 181 | try { 182 | const prettyDoc = documents.get(doc.uri); 183 | if (prettyDoc) { 184 | prettyDoc.refresh(); 185 | } else if (registry) { 186 | const scopeName = getLanguageScopeName(doc.languageId); 187 | if (scopeName) { 188 | const grammar = await registry.loadGrammar(scopeName); 189 | documents.set(doc.uri, new DocumentController(doc, grammar)); 190 | } 191 | } 192 | } catch (err) {} 193 | } 194 | 195 | function closeDocument(doc: vscode.TextDocument) { 196 | const prettyDoc = documents.get(doc.uri); 197 | if (prettyDoc) { 198 | prettyDoc.dispose(); 199 | documents.delete(doc.uri); 200 | } 201 | } 202 | 203 | function unloadDocuments() { 204 | for (const prettyDoc of documents.values()) { 205 | prettyDoc.dispose(); 206 | } 207 | documents.clear(); 208 | } 209 | 210 | /** clean-up; this extension is being unloaded */ 211 | export function deactivate() { 212 | unloadDocuments(); 213 | } 214 | --------------------------------------------------------------------------------