├── .gitignore ├── CHANGELOG.md ├── .vscodeignore ├── tsconfig.json ├── .gitattributes ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── README.md ├── LICENSE ├── test ├── index.ts └── extension.test.ts ├── src ├── scope-info.d.ts ├── text-mate.ts ├── document.ts ├── text-util.ts └── extension.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | typings 4 | *.vsix -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | * extension API 3 | 4 | ## 0.1.0 5 | * Initial release -------------------------------------------------------------------------------- /.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 | "moduleResolution": "node", 5 | "target": "ES6", 6 | "lib": [ "es6" ], 7 | "outDir": "out", 8 | "sourceMap": true, 9 | "allowUnreachableCode": false, 10 | "allowUnusedLabels": false, 11 | "noFallthroughCasesInSwitch": true, 12 | "noLib": false, 13 | "pretty": true, 14 | "rootDir": "." 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version 10 | "editor.rulers": [80], 11 | "workbench.editor.showTabs": true, 12 | "editor.tabSize": 2, 13 | "editor.insertSpaces": true, 14 | "terminal.integrated.shell.windows": "C:\\cygwin64\\cygwin.bat", // "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell_ise.exe" 15 | "files.eol": "\n" 16 | } -------------------------------------------------------------------------------- /.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: -------------------------------------------------------------------------------- 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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scope Info 2 | 3 | An extension for vscode that provides TextMate scope information. 4 | 5 | ## For users 6 | 7 | To view the TextMate scope information of your code, run `scope-info: enable hover information`; to disable, run `scope-info: disable hover information`. 8 | 9 | ## For extension authors 10 | 11 | This extension provides an API by which your extension can query scope & token information. Refer to [scope-info.d.ts](https://github.com/siegebell/scope-info/blob/master/src/scope-info.d.ts) and [extension.test.ts](https://github.com/siegebell/scope-info/blob/master/test/extension.test.ts) for more details. Example usage: 12 | ```TypeScript 13 | import * as vscode from 'vscode'; 14 | import * as scopeInfo from 'scope-info'; 15 | 16 | async function example(doc : vscode.TextDocument, pos: vscode.Position) : void { 17 | const siExt = vscode.extensions.getExtension('siegebell.scope-info'); 18 | const si = await siExt.activate(); 19 | const token : scopeInfo.Token = si.getScopeAt(doc, pos); 20 | } 21 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christian J. Bell 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /src/scope-info.d.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | /** 4 | * A grammar 5 | */ 6 | export interface IGrammar { 7 | /** 8 | * Tokenize `lineText` using previous line state `prevState`. 9 | */ 10 | tokenizeLine(lineText: string, prevState: StackElement): ITokenizeLineResult; 11 | } 12 | export interface ITokenizeLineResult { 13 | readonly tokens: IToken[]; 14 | /** 15 | * The `prevState` to be passed on to the next line tokenization. 16 | */ 17 | readonly ruleStack: StackElement; 18 | } 19 | export interface IToken { 20 | startIndex: number; 21 | readonly endIndex: number; 22 | readonly scopes: string[]; 23 | } 24 | /** 25 | * **IMPORTANT** - Immutable! 26 | */ 27 | export interface StackElement { 28 | equals(other: StackElement): boolean; 29 | } 30 | 31 | 32 | export interface Token { 33 | range: vscode.Range, 34 | text: string, 35 | scopes: string[]; 36 | } 37 | 38 | export interface ScopeInfoAPI { 39 | getScopeAt(document: vscode.TextDocument, position: vscode.Position) : Token|null; 40 | getGrammar(scopeName: string) : Promise; 41 | getScopeForLanguage(language: string) : string|null; 42 | } 43 | 44 | export function activate() : Thenable { 45 | const siExt = vscode.extensions.getExtension('siegebell.scope-info'); 46 | return siExt.activate(); 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scope-info", 3 | "displayName": "Scope Info", 4 | "description": "Displays the TextMate scope when you hover over text.", 5 | "version": "0.2.0", 6 | "publisher": "siegebell", 7 | "license": "MIT", 8 | "engines": { 9 | "vscode": "^1.7.0" 10 | }, 11 | "categories": [ 12 | "Other" 13 | ], 14 | "keywords": [ 15 | "TextMate", "scope", "grammar" 16 | ], 17 | "activationEvents": [ 18 | "onCommand:extension.enableScopeHover" 19 | ], 20 | "main": "./out/src/extension", 21 | "contributes": { 22 | "commands": [ 23 | { 24 | "command": "extension.enableScopeHover", 25 | "title": "enable hover information", 26 | "category": "scope-info" 27 | }, 28 | { 29 | "command": "extension.disableScopeHover", 30 | "title": "disable hover information", 31 | "category": "scope-info" 32 | } 33 | ] 34 | }, 35 | "scripts": { 36 | "vscode:prepublish": "tsc -p ./", 37 | "postinstall": "node ./node_modules/vscode/bin/install", 38 | "watch": "tsc -watch -p ./", 39 | "compile": "tsc -p ./" 40 | }, 41 | "dependencies": { 42 | }, 43 | "devDependencies": { 44 | "typescript": "2.1.4", 45 | "vscode": "^1.0.3", 46 | "mocha": "3.2.0", 47 | "@types/mocha": "2.2.33", 48 | "@types/node": "^6.0.40" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/siegebell/scope-info/issues" 52 | }, 53 | "homepage": "https://github.com/siegebell/scope-info/blob/master/README.md", 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/siegebell/scope-info.git" 57 | } 58 | } -------------------------------------------------------------------------------- /src/text-mate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | const tm = require(path.join(require.main.filename, '../../node_modules/vscode-textmate/release/main.js')); 3 | 4 | export function matchScope(scope: string, scopes: string[]) : boolean { 5 | if(!scope) 6 | return true; 7 | const parts = scope.split(/\s+/); 8 | let idx = 0; 9 | for(let part of parts) { 10 | while(idx < scopes.length && !scopes[idx].startsWith(part)) 11 | ++idx; 12 | if(idx >= scopes.length) 13 | return false; 14 | ++idx; 15 | } 16 | return true; 17 | } 18 | 19 | 20 | // export type Registry = N.Registry; 21 | export const Registry : Registry = tm.Registry; 22 | export interface Registry { 23 | new (locator?: IGrammarLocator); 24 | /** 25 | * Load the grammar for `scopeName` and all referenced included grammars asynchronously. 26 | */ 27 | loadGrammar(initialScopeName: string, callback: (err: any, grammar: IGrammar) => void): void; 28 | /** 29 | * Load the grammar at `path` synchronously. 30 | */ 31 | loadGrammarFromPathSync(path: string): IGrammar; 32 | /** 33 | * Get the grammar for `scopeName`. The grammar must first be created via `loadGrammar` or `loadGrammarFromPathSync`. 34 | */ 35 | grammarForScopeName(scopeName: string): IGrammar; 36 | } 37 | 38 | /** 39 | * A registry helper that can locate grammar file paths given scope names. 40 | */ 41 | export interface IGrammarLocator { 42 | getFilePath(scopeName: string): string; 43 | getInjections?(scopeName: string): string[]; 44 | } 45 | 46 | export interface IGrammarInfo { 47 | readonly fileTypes: string[]; 48 | readonly name: string; 49 | readonly scopeName: string; 50 | readonly firstLineMatch: string; 51 | } 52 | /** 53 | * A grammar 54 | */ 55 | export interface IGrammar { 56 | /** 57 | * Tokenize `lineText` using previous line state `prevState`. 58 | */ 59 | tokenizeLine(lineText: string, prevState: StackElement): ITokenizeLineResult; 60 | } 61 | export interface ITokenizeLineResult { 62 | readonly tokens: IToken[]; 63 | /** 64 | * The `prevState` to be passed on to the next line tokenization. 65 | */ 66 | readonly ruleStack: StackElement; 67 | } 68 | export interface IToken { 69 | startIndex: number; 70 | readonly endIndex: number; 71 | readonly scopes: string[]; 72 | } 73 | /** 74 | * **IMPORTANT** - Immutable! 75 | */ 76 | export interface StackElement { 77 | _stackElementBrand: void; 78 | readonly _parent: StackElement; 79 | equals(other: StackElement): boolean; 80 | } 81 | -------------------------------------------------------------------------------- /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 | import * as path from 'path'; 9 | import * as util from 'util'; 10 | 11 | // You can import and use all API from the 'vscode' module 12 | // as well as import your extension to test it 13 | import * as vscode from 'vscode'; 14 | import * as ext from '../src/extension'; 15 | import * as api from '../src/scope-info'; 16 | 17 | // Defines a Mocha test suite to group tests of similar kind together 18 | suite("Scope Info", function () { 19 | let si : api.ScopeInfoAPI; 20 | test("api - getScopeForLanguage", async function() { 21 | const siExt = vscode.extensions.getExtension('siegebell.scope-info'); 22 | si = await siExt.activate(); 23 | assert.equal(si.getScopeForLanguage("html"), "text.html.basic"); 24 | }); 25 | 26 | test("api - getGrammar", async function() { 27 | const siExt = vscode.extensions.getExtension('siegebell.scope-info'); 28 | si = await siExt.activate(); 29 | const g = await si.getGrammar("text.html.basic"); 30 | const t = g.tokenizeLine("", null); 31 | assert.equal(t.tokens.length, 11); 32 | assert.deepStrictEqual(t.tokens[0], {startIndex: 0, endIndex: 2, scopes: ['text.html.basic','meta.tag.sgml.html','punctuation.definition.tag.html']}); 33 | assert.deepStrictEqual(t.tokens[1], {startIndex: 2, endIndex: 9, scopes: ['text.html.basic','meta.tag.sgml.html','meta.tag.sgml.doctype.html']}); 34 | assert.deepStrictEqual(t.tokens[2], {startIndex: 9, endIndex: 14, scopes: ['text.html.basic','meta.tag.sgml.html','meta.tag.sgml.doctype.html']}); 35 | assert.deepStrictEqual(t.tokens[7], {startIndex: 21, endIndex: 22, scopes: ['text.html.basic','meta.tag.any.html','punctuation.definition.tag.html','meta.scope.between-tag-pair.html']}); 36 | }); 37 | 38 | test("api - getScopeAt", async function() { 39 | const siExt = vscode.extensions.getExtension('siegebell.scope-info'); 40 | si = await siExt.activate(); 41 | const file = vscode.Uri.parse('untitled:C:\test.html'); 42 | const doc = await vscode.workspace.openTextDocument(file); 43 | const ed = await vscode.window.showTextDocument(doc); 44 | await ed.edit(builder => { 45 | builder.insert(new vscode.Position(0,0), "\n") 46 | }) 47 | const t1 = si.getScopeAt(doc, new vscode.Position(0,2)); 48 | assert.equal(t1.text, "DOCTYPE"); 49 | assert.deepStrictEqual(t1.range, new vscode.Range(0,2,0,9)); 50 | assert.deepStrictEqual(t1.scopes, ['text.html.basic','meta.tag.sgml.html','meta.tag.sgml.doctype.html']); 51 | }); 52 | 53 | }); -------------------------------------------------------------------------------- /src/document.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as textUtil from './text-util'; 3 | import * as tm from './text-mate'; 4 | import * as api from './scope-info'; 5 | 6 | const debugging = false; 7 | const activeEditorDecorationTimeout = 20; 8 | const inactiveEditorDecorationTimeout = 200; 9 | 10 | 11 | export class DocumentController implements vscode.Disposable { 12 | private subscriptions : vscode.Disposable[] = []; 13 | 14 | // Stores the state for each line 15 | private grammarState : tm.StackElement[] = []; 16 | private grammar : tm.IGrammar; 17 | 18 | public constructor(doc: vscode.TextDocument, textMateGrammar: tm.IGrammar, 19 | private document = doc, 20 | ) { 21 | this.grammar = textMateGrammar; 22 | 23 | // Parse whole document 24 | const docRange = new vscode.Range(0,0,this.document.lineCount,0); 25 | this.reparsePretties(docRange); 26 | 27 | this.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e) => { 28 | if(e.document == this.document) 29 | this.onChangeDocument(e); 30 | })); 31 | } 32 | 33 | public dispose() { 34 | this.subscriptions.forEach((s) => s.dispose()); 35 | } 36 | 37 | 38 | private refreshTokensOnLine(line: vscode.TextLine) : {tokens: tm.IToken[], invalidated: boolean} { 39 | if(!this.grammar) 40 | return {tokens: [], invalidated: false}; 41 | const prevState = this.grammarState[line.lineNumber-1] || null; 42 | const lineTokens = this.grammar.tokenizeLine(line.text, prevState); 43 | const invalidated = !this.grammarState[line.lineNumber] || !lineTokens.ruleStack.equals(this.grammarState[line.lineNumber]) 44 | this.grammarState[line.lineNumber] = lineTokens.ruleStack; 45 | return {tokens: lineTokens.tokens, invalidated: invalidated}; 46 | } 47 | 48 | public getScopeAt(position: vscode.Position) : api.Token|null { 49 | if(!this.grammar) 50 | 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 {range: new vscode.Range(position.line,t.startIndex,position.line,t.endIndex), text: line.text.substring(t.startIndex,t.endIndex), scopes: t.scopes } 58 | } 59 | return null; 60 | } 61 | 62 | private reparsePretties(range: vscode.Range) : void { 63 | range = this.document.validateRange(range); 64 | 65 | const startCharacter = 0; 66 | 67 | let invalidatedTokenState = false; 68 | 69 | // Collect new pretties 70 | const lineCount = this.document.lineCount; 71 | let lineIdx; 72 | for(lineIdx = range.start.line; lineIdx <= range.end.line || (invalidatedTokenState && lineIdx < lineCount); ++lineIdx) { 73 | const line = this.document.lineAt(lineIdx); 74 | const {tokens: tokens, invalidated: invalidated} = this.refreshTokensOnLine(line); 75 | invalidatedTokenState = invalidated; 76 | } 77 | } 78 | 79 | private applyChanges(changes: vscode.TextDocumentContentChangeEvent[]) { 80 | const sortedChanges = 81 | changes.sort((change1,change2) => change1.range.start.isAfter(change2.range.start) ? -1 : 1) 82 | for(const change of sortedChanges) { 83 | try { 84 | const delta = textUtil.toRangeDelta(change.range, change.text); 85 | const editRange = textUtil.rangeDeltaNewRange(delta); 86 | 87 | const reparsed = this.reparsePretties(editRange); 88 | } catch(e) { 89 | console.error(e); 90 | } 91 | } 92 | } 93 | 94 | private onChangeDocument(event: vscode.TextDocumentChangeEvent) { 95 | this.applyChanges(event.contentChanges); 96 | } 97 | 98 | public refresh() { 99 | this.grammarState = []; 100 | const docRange = new vscode.Range(0,0,this.document.lineCount,0); 101 | this.reparsePretties(docRange); 102 | } 103 | } -------------------------------------------------------------------------------- /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 | 7 | export interface RangeDelta { 8 | start: vscode.Position; 9 | end: vscode.Position; 10 | linesDelta: number; 11 | endCharactersDelta: number; // delta for positions on the same line as the end position 12 | } 13 | 14 | /** 15 | * @returns the Position (line, column) for the location (character position) 16 | */ 17 | function positionAt(text: string, offset: number) : vscode.Position { 18 | if(offset > text.length) 19 | offset = text.length; 20 | let line = 0; 21 | let lastIndex = 0; 22 | while(true) { 23 | const match = lineEndingRE.exec(text.substring(lastIndex)); 24 | if(lastIndex + match[1].length >= offset) 25 | return new vscode.Position(line, offset - lastIndex) 26 | lastIndex+= match[0].length; 27 | ++line; 28 | } 29 | } 30 | 31 | /** 32 | * @returns the lines and characters represented by the text 33 | */ 34 | export function toRangeDelta(oldRange:vscode.Range, text: string) : RangeDelta { 35 | const newEnd = positionAt(text,text.length); 36 | let charsDelta; 37 | if(oldRange.start.line == oldRange.end.line) 38 | charsDelta = newEnd.character - (oldRange.end.character-oldRange.start.character); 39 | else 40 | charsDelta = newEnd.character - oldRange.end.character; 41 | 42 | return { 43 | start: oldRange.start, 44 | end: oldRange.end, 45 | linesDelta: newEnd.line-(oldRange.end.line-oldRange.start.line), 46 | endCharactersDelta: charsDelta 47 | }; 48 | } 49 | 50 | export function rangeDeltaNewRange(delta: RangeDelta) : vscode.Range { 51 | let x : number; 52 | if (delta.linesDelta > 0) 53 | x = delta.endCharactersDelta; 54 | else if (delta.linesDelta < 0 && delta.start.line == delta.end.line + delta.linesDelta) 55 | x = delta.end.character + delta.endCharactersDelta + delta.start.character; 56 | else 57 | x = delta.end.character + delta.endCharactersDelta; 58 | return new vscode.Range(delta.start, new vscode.Position(delta.end.line + delta.linesDelta, x)); 59 | } 60 | 61 | function positionRangeDeltaTranslate(pos: vscode.Position, delta: RangeDelta) : vscode.Position { 62 | if(pos.isBefore(delta.end)) 63 | return pos; 64 | else if (delta.end.line == pos.line) { 65 | let x = pos.character + delta.endCharactersDelta; 66 | if (delta.linesDelta > 0) 67 | x = x - delta.end.character; 68 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 69 | x = x + delta.start.character; 70 | return new vscode.Position(pos.line + delta.linesDelta, x); 71 | } 72 | else // if(pos.line > delta.end.line) 73 | return new vscode.Position(pos.line + delta.linesDelta, pos.character); 74 | } 75 | 76 | function positionRangeDeltaTranslateEnd(pos: vscode.Position, delta: RangeDelta) : vscode.Position { 77 | if(pos.isBeforeOrEqual(delta.end)) 78 | return pos; 79 | else if (delta.end.line == pos.line) { 80 | let x = pos.character + delta.endCharactersDelta; 81 | if (delta.linesDelta > 0) 82 | x = x - delta.end.character; 83 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 84 | x = x + delta.start.character; 85 | return new vscode.Position(pos.line + delta.linesDelta, x); 86 | } 87 | else // if(pos.line > delta.end.line) 88 | return new vscode.Position(pos.line + delta.linesDelta, pos.character); 89 | } 90 | 91 | export function rangeTranslate(range: vscode.Range, delta: RangeDelta) { 92 | return new vscode.Range( 93 | positionRangeDeltaTranslate(range.start, delta), 94 | positionRangeDeltaTranslateEnd(range.end, delta) 95 | ) 96 | } 97 | 98 | export function rangeContains(range: vscode.Range, pos: vscode.Position, exclStart=false, inclEnd=false) { 99 | return range.start.isBeforeOrEqual(pos) 100 | && (!exclStart || !range.start.isEqual(pos)) 101 | && ((inclEnd && range.end.isEqual(pos)) || range.end.isAfter(pos)); 102 | } 103 | 104 | export function maxPosition(x: vscode.Position, y: vscode.Position) { 105 | if(x.line < y.line) 106 | return x; 107 | if(x.line < x.line) 108 | return y; 109 | if(x.character < y.character) 110 | return x; 111 | else 112 | return y; 113 | } 114 | -------------------------------------------------------------------------------- /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 util from 'util'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | import {DocumentController} from './document'; 8 | import * as tm from './text-mate'; 9 | import * as api from './scope-info'; 10 | 11 | /** Tracks all documents that substitutions are being applied to */ 12 | let documents = new Map(); 13 | 14 | export let textMateRegistry : tm.Registry; 15 | 16 | interface ExtensionGrammar { 17 | language?: string, scopeName?: string, path?: string, embeddedLanguages?: {[scopeName:string]:string}, injectTo?: string[] 18 | } 19 | interface ExtensionPackage { 20 | contributes?: { 21 | languages?: {id: string, configuration: string}[], 22 | grammars?: ExtensionGrammar[], 23 | } 24 | } 25 | 26 | function getLanguageScopeName(languageId: string) : string { 27 | try { 28 | const languages = 29 | vscode.extensions.all 30 | .filter(x => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars) 31 | .reduce((a: ExtensionGrammar[],b) => [...a, ...(b.packageJSON as ExtensionPackage).contributes.grammars], []); 32 | const matchingLanguages = languages.filter(g => g.language === languageId); 33 | 34 | if(matchingLanguages.length > 0) { 35 | // console.info(`Mapping language ${languageId} to initial scope ${matchingLanguages[0].scopeName}`); 36 | return matchingLanguages[0].scopeName; 37 | } 38 | } catch(err) { } 39 | return undefined; 40 | } 41 | 42 | const grammarLocator : tm.IGrammarLocator = { 43 | getFilePath: function(scopeName: string) : string { 44 | try { 45 | const grammars = 46 | vscode.extensions.all 47 | .filter(x => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars) 48 | .reduce((a: (ExtensionGrammar&{extensionPath: string})[],b) => [...a, ...(b.packageJSON as ExtensionPackage).contributes.grammars.map(x => Object.assign({extensionPath: b.extensionPath}, x))], []); 49 | const matchingLanguages = grammars.filter(g => g.scopeName === scopeName); 50 | // let match : RegExpExecArray; 51 | // if(matchingLanguages.length === 0 && (match = /^source[.](.*)/.exec(scopeName))) 52 | // matchingLanguages = grammars.filter(g => g.language === match[1]); 53 | 54 | if(matchingLanguages.length > 0) { 55 | const ext = matchingLanguages[0]; 56 | const file = path.join(ext.extensionPath, ext.path); 57 | console.info(`Scope-info: found grammar for ${scopeName} at ${file}`) 58 | return file; 59 | } 60 | } catch(err) { } 61 | return undefined; 62 | } 63 | } 64 | 65 | async function provideHoverInfo(subscriptions: vscode.Disposable[]) { 66 | const allLanguages = 67 | (await vscode.languages.getLanguages()) 68 | .filter(x => getLanguageScopeName(x) !== undefined); 69 | 70 | 71 | subscriptions.push(vscode.languages.registerHoverProvider(allLanguages, {provideHover: (doc,pos,tok) : vscode.Hover => { 72 | if(!isHoverEnabled()) 73 | return; 74 | try { 75 | const prettyDoc = documents.get(doc.uri); 76 | if(prettyDoc) { 77 | const token = prettyDoc.getScopeAt(pos); 78 | if(token) 79 | return {contents: [`Token: \`${token.text}\``, ...token.scopes], range: token.range} 80 | } 81 | } catch(err) { 82 | } 83 | return undefined; 84 | }})); 85 | } 86 | 87 | export let workspaceState : vscode.Memento; 88 | 89 | /** initialize everything; main entry point */ 90 | export function activate(context: vscode.ExtensionContext) : api.ScopeInfoAPI { 91 | workspaceState = context.workspaceState; 92 | 93 | function registerCommand(commandId:string, run:(...args:any[])=>void): void { 94 | context.subscriptions.push(vscode.commands.registerCommand(commandId, run)); 95 | } 96 | 97 | registerCommand('extension.disableScopeHover', disableScopeHover); 98 | registerCommand('extension.enableScopeHover', enableScopeHover); 99 | 100 | context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(openDocument)); 101 | context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(closeDocument)); 102 | 103 | provideHoverInfo(context.subscriptions); 104 | 105 | reloadGrammar(); 106 | 107 | const api : api.ScopeInfoAPI = { 108 | getScopeAt(document: vscode.TextDocument, position: vscode.Position) : api.Token|null { 109 | try { 110 | const prettyDoc = documents.get(document.uri); 111 | if(prettyDoc) { 112 | return prettyDoc.getScopeAt(position); 113 | } 114 | } catch(err) { 115 | } 116 | return null; 117 | }, 118 | getScopeForLanguage(language: string) : string|null { 119 | return getLanguageScopeName(language) || null; 120 | }, 121 | async getGrammar(scopeName: string) : Promise { 122 | try { 123 | if(textMateRegistry) 124 | return await loadGrammar(scopeName); 125 | } catch(err) { } 126 | return null; 127 | } 128 | } 129 | return api; 130 | } 131 | 132 | let hoverEnabled = false; 133 | export function isHoverEnabled() : boolean { 134 | return hoverEnabled; 135 | // return workspaceState.get('showHoverInfo', false as boolean) === true; 136 | } 137 | 138 | export function setHover(enabled: boolean) : void { 139 | hoverEnabled = enabled; 140 | // workspaceState.update('showHoverInfo', enabled); 141 | } 142 | 143 | /** Re-read the settings and recreate substitutions for all documents */ 144 | function reloadGrammar() { 145 | try { 146 | textMateRegistry = new tm.Registry(grammarLocator); 147 | } catch(err) { 148 | textMateRegistry = undefined; 149 | console.error(err); 150 | } 151 | 152 | // Recreate the documents 153 | unloadDocuments(); 154 | for(const doc of vscode.workspace.textDocuments) 155 | openDocument(doc); 156 | } 157 | 158 | function disableScopeHover() { 159 | setHover(false); 160 | unloadDocuments(); 161 | } 162 | 163 | function enableScopeHover() { 164 | setHover(true); 165 | reloadGrammar(); 166 | } 167 | 168 | 169 | function loadGrammar(scopeName: string) : Promise { 170 | return new Promise((resolve,reject) => { 171 | try { 172 | textMateRegistry.loadGrammar(scopeName, (err, grammar) => { 173 | if(err) 174 | reject(err) 175 | else 176 | resolve(grammar); 177 | }) 178 | } catch(err) { 179 | reject(err); 180 | } 181 | }) 182 | } 183 | 184 | async function openDocument(doc: vscode.TextDocument) { 185 | try { 186 | const prettyDoc = documents.get(doc.uri); 187 | if(prettyDoc) { 188 | prettyDoc.refresh(); 189 | } else if(textMateRegistry) { 190 | const scopeName = getLanguageScopeName(doc.languageId); 191 | if(scopeName) { 192 | const grammar = await loadGrammar(scopeName); 193 | documents.set(doc.uri, new DocumentController(doc, grammar)); 194 | } 195 | } 196 | } catch(err) { 197 | } 198 | } 199 | 200 | function closeDocument(doc: vscode.TextDocument) { 201 | const prettyDoc = documents.get(doc.uri); 202 | if(prettyDoc) { 203 | prettyDoc.dispose(); 204 | documents.delete(doc.uri); 205 | } 206 | } 207 | 208 | function unloadDocuments() { 209 | for(const prettyDoc of documents.values()) { 210 | prettyDoc.dispose(); 211 | } 212 | documents.clear(); 213 | } 214 | 215 | /** clean-up; this extension is being unloaded */ 216 | export function deactivate() { 217 | unloadDocuments(); 218 | } 219 | 220 | --------------------------------------------------------------------------------