├── CHANGELOG.md ├── .gitignore ├── .vscodeignore ├── tsconfig.json ├── .eslintrc.json ├── test ├── suite │ ├── document.test.ts │ ├── DisjointRangeSet.test.ts │ ├── index.ts │ ├── text-mate.test.ts │ ├── PrettyModel.test.ts │ ├── vscode-shunt.ts │ └── RangeSet.test.ts └── runTest.ts ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .github └── workflows │ ├── ci.yml │ └── cd.yml ├── LICENSE ├── src ├── api.ts ├── regexp-iteration.ts ├── position.ts ├── configuration.ts ├── text-mate.ts ├── text-util.ts ├── decorations.ts ├── RangeSet.ts ├── document.ts ├── extension.ts ├── DisjointRangeSet.ts ├── sregexp.ts └── PrettyModel.ts └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 2 | * Initial release 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | typings 4 | *.vsix 5 | package-lock.json 6 | yarn-error.log 7 | .vscode-test/ 8 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/suite/document.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 | //import * as pdoc from '../../src/document'; 7 | 8 | 9 | // Defines a Mocha test suite to group tests of similar kind together 10 | suite("PrettyDocumentController", () => { 11 | //let doc : pdoc.PrettyDocumentController; 12 | // beforeEach(function() { 13 | // const textDoc = vscode.TextDocument(); 14 | 15 | // doc = new pdoc.PrettyDocumentController(textDoc, langSettings, opts); 16 | // }) 17 | 18 | // Defines a Mocha unit test 19 | }); -------------------------------------------------------------------------------- /.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": false, 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 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | tags: 5 | - "v*.*.*" 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-extension: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Install Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | - run: 23 | yarn run package 24 | run-tests: 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | - name: Install Node.js 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: 16.x 36 | - run: 37 | yarn install && xvfb-run yarn test 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | 6 | jobs: 7 | publish-extension: 8 | runs-on: ubuntu-latest 9 | if: success() && startsWith(github.ref, 'refs/tags/') 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Install Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 16.x 17 | - name: Publish to Open VSX Registry 18 | uses: HaaLeo/publish-vscode-extension@v1 19 | id: publishToOpenVSX 20 | with: 21 | pat: ${{ secrets.OVSX_PAT }} 22 | packagePath: ./ 23 | yarn: true 24 | preRelease: true 25 | - name: Publish to Visual Studio Marketplace 26 | uses: HaaLeo/publish-vscode-extension@v1 27 | with: 28 | pat: ${{ secrets.VSCE_PAT }} 29 | packagePath: ./ 30 | registryUrl: https://marketplace.visualstudio.com 31 | extensionFile: ${{ steps.publishToOpenVSX.outputs.vsixPath }} 32 | yarn: true 33 | preRelease: true 34 | -------------------------------------------------------------------------------- /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/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as tmp from 'tmp-promise'; 3 | 4 | import { runTests } from '@vscode/test-electron'; 5 | 6 | async function main() { 7 | try { 8 | // The folder containing the Extension Manifest package.json 9 | // Passed to `--extensionDevelopmentPath` 10 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 11 | 12 | // The path to test runner 13 | // Passed to --extensionTestsPath 14 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 15 | 16 | const storagePath = await tmp.dir(); 17 | const userDataDir = path.join(storagePath.path, 'settings'); 18 | // const userSettingsPath = path.join(userDataDir, 'User'); 19 | const launchArgs = [path.resolve(__dirname, '../../testFixture'), "--disable-extensions", "--user-data-dir=" + userDataDir]; 20 | 21 | // Download VS Code, unzip it and run the integration test 22 | await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs }); 23 | } catch (err) { 24 | console.error('Failed to run tests'); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | main(); -------------------------------------------------------------------------------- /test/suite/DisjointRangeSet.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 | // import * as vscode from './vscode-shunt'; 10 | // const drs = proxyquire('../src/DisjointRangeSet', {'vscode': {extname: function(file){return './vscode-shunt'}, '@global': true}}); 11 | import * as vscode from 'vscode'; 12 | import * as drs from '../../src/DisjointRangeSet'; 13 | 14 | 15 | // Defines a Mocha test suite to group tests of similar kind together 16 | suite("DisjointRangeSet", () => { 17 | 18 | // Defines a Mocha unit test 19 | test("insert", () => { 20 | const x1 = new drs.DisjointRangeSet(); 21 | const r1 = new vscode.Range(1,9,1,14); 22 | const r2 = new vscode.Range(1,14,1,16); 23 | assert.strictEqual(x1.insert(r1), true); 24 | assert.strictEqual(x1.insert(r1), false); 25 | assert.strictEqual(x1.insert(new vscode.Range(1,9,1,10)), false); 26 | assert.strictEqual(x1.insert(new vscode.Range(1,9,1,20)), false); 27 | assert.strictEqual(x1.insert(new vscode.Range(0,0,1,20)), false); 28 | assert.deepStrictEqual(x1.getRanges(), [r1]); 29 | assert.strictEqual(x1.insert(r2), true); 30 | assert.deepStrictEqual(x1.getRanges(), [r1,r2]); 31 | }); 32 | }); -------------------------------------------------------------------------------- /.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": { 27 | "_id": "build", 28 | "isDefault": false 29 | } 30 | }, 31 | { 32 | "label": "test", 33 | "type": "shell", 34 | "args": [ 35 | "test" 36 | ], 37 | "problemMatcher": [], 38 | "group": { 39 | "_id": "test", 40 | "isDefault": false 41 | } 42 | }, 43 | { 44 | "label": "test-debug", 45 | "type": "shell", 46 | "args": [ 47 | "run", 48 | "test-debug" 49 | ], 50 | "problemMatcher": [] 51 | } 52 | ] 53 | 54 | } -------------------------------------------------------------------------------- /test/suite/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 | import * as path from 'path'; 14 | import * as Mocha from 'mocha'; 15 | import * as glob from 'glob'; 16 | 17 | export function run(): Promise { 18 | // Create the mocha test 19 | const mocha = new Mocha({ 20 | ui: 'tdd', 21 | }); 22 | 23 | const testsRoot = path.resolve(__dirname, '..'); 24 | 25 | return new Promise((c, e) => { 26 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 27 | if (err) { 28 | return e(err); 29 | } 30 | 31 | // Add files to the test suite 32 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 33 | 34 | try { 35 | // Run the mocha test 36 | mocha.run(failures => { 37 | if (failures > 0) { 38 | e(new Error(`${failures} tests failed.`)); 39 | } else { 40 | c(); 41 | } 42 | }); 43 | } catch (err) { 44 | console.error(err); 45 | e(err); 46 | } 47 | }); 48 | }); 49 | } -------------------------------------------------------------------------------- /.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 | "stopOnEntry": false, 25 | "sourceMaps": true, 26 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 27 | "preLaunchTask": "watch" 28 | } 29 | // { 30 | // "name": "Debug Mocha Tests", 31 | // "type": "node", 32 | // "sourceMaps": true, 33 | // "request": "attach", 34 | // "port": 5858, 35 | // "outFiles": [ 36 | // "${workspaceRoot}/out/**/*.js" 37 | // ], 38 | // "preLaunchTask": "test-debug" 39 | // } 40 | // { 41 | // "name": "Launch Tests", 42 | // "type": "extensionHost", 43 | // "request": "launch", 44 | // "port": 5858, 45 | // "runtimeExecutable": "${execPath}", 46 | // "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 47 | // "stopOnEntry": false, 48 | // "sourceMaps": true, 49 | // "outFiles": [ 50 | // "${workspaceRoot}/out/test/**/*.js" 51 | // ], 52 | // "preLaunchTask": "npm" 53 | // } 54 | ] 55 | } -------------------------------------------------------------------------------- /test/suite/text-mate.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 | // const proxyquire = require('proxyquire').noCallThru(); 10 | // const textMateModule = path.join(require.main.filename, '../../node_modules/vscode-textmate/release/main.js'); 11 | // let mockModules = {}; 12 | // mockModules['vscode'] = {extname: function(file){return './vscode-shunt'}, '@global': true}; 13 | // mockModules[textMateModule] = {extname: function(file){return './vscode-shunt'}, '@global': true}; 14 | // const tm = proxyquire('../src/text-mate', mockModules); 15 | import * as tm from '../../src/text-mate'; 16 | 17 | // Defines a Mocha test suite to group tests of similar kind together 18 | suite("text-mate", () => { 19 | function mt(x,y,s) { 20 | return {startIndex: x, endIndex: y, scopes: s.split(' ')} 21 | } 22 | test("combineIdenticalTokenScopes", () => { 23 | function test(x,y) { 24 | assert.deepStrictEqual(tm.combineIdenticalTokenScopes(x), y); 25 | } 26 | test([], []); 27 | test([mt(0,2,"a b")], [mt(0,2,"a b")]); 28 | test([mt(0,2,"a b"), mt(2,4,"a b")], [mt(0,4,"a b")]); 29 | test([mt(0,2,"a b"), mt(3,4,"a b")], [mt(0,2,"a b"),mt(3,4,"a b")]); 30 | test([mt(0,2,"c b"), mt(2,4,"a b")], [mt(0,2,"c b"), mt(2,4,"a b")]); 31 | test([mt(0,2,"a b"), mt(2,4,"a b"), mt(4,6,"a b")], [mt(0,6,"a b")]); 32 | test([mt(0,2,"a b"), mt(2,4,"a b"), mt(4,6,"c a b")], [mt(0,4,"a b"),mt(4,6,"c a b")]); 33 | test([mt(0,2,"a b"), mt(2,4,"c a b"), mt(4,6,"c a b")], [mt(0,2,"a b"),mt(2,6,"c a b")]); 34 | test([mt(0, 4, "source.fsharp"),mt(4, 5, "source.fsharp constant.numeric.integer.nativeint.fsharp"),mt(5, 6, "source.fsharp"),mt(6, 45, "source.fsharp comment.line.double-slash.fsharp")], 35 | [mt(0, 4, "source.fsharp"),mt(4, 5, "source.fsharp constant.numeric.integer.nativeint.fsharp"),mt(5, 6, "source.fsharp"),mt(6, 45, "source.fsharp comment.line.double-slash.fsharp")]); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** Essentially mirrors vscode.DecorationRenderOptions, but restricted to the 4 | * properties that apply to both :before/:after decorations and plain decorations */ 5 | export interface ConcealStyleProperties { 6 | border?: string, 7 | textDecoration?: string, 8 | color?: string, 9 | backgroundColor?: string, 10 | hackCSS?: string, 11 | } 12 | export interface ConcealStyle extends ConcealStyleProperties { 13 | dark?: ConcealStyleProperties, 14 | light?: ConcealStyleProperties, 15 | } 16 | 17 | /** An individual substitution */ 18 | export interface Substitution { 19 | ugly: string; // regular expression describing the text to replace 20 | pretty: string; // plain-text symbol to show instead 21 | pre?: string; // regular expression guard on text before "ugly" 22 | post?: string; // regular expression guard on text after "ugly" 23 | style?: ConcealStyle; // stylings to apply to the pretty text, if specified, or else the ugly text 24 | scope?: string, // TextMate scope; if specified, the ugly portion's range must be exactly within the given scope 25 | } 26 | 27 | export interface LanguageEntry { 28 | /** language(s) to apply these substitutions on */ 29 | language: vscode.DocumentSelector; 30 | /** substitution rules */ 31 | substitutions: Substitution[]; 32 | /** The filename of the language's grammar. If specified, substitutions' scope entries refer to scope names in the given grammar. If undefined, then search for a grammar.*/ 33 | textMateGrammar?: string; 34 | /** Override the TextMate scope for the language */ 35 | textMateInitialScope?: string; 36 | } 37 | 38 | 39 | export interface ConcealSymbolsMode { 40 | /** Register a handler to receive notifications when PSM is enabled or disabled. 41 | * @returns a disposable object to unregister the handler 42 | */ 43 | onDidEnabledChange: (handler: (enabled: boolean) => void) => vscode.Disposable, 44 | /** Query whether PSM is "enabled" - this refers to the user's ability to 45 | * temporarily enable/disable the mode for an instance of vscode." 46 | * @returns `true` iff PSM is currently enabled 47 | */ 48 | isEnabled: () => boolean, 49 | /** Temporarily add more substitutions. 50 | * @returns a disposable object to remove the provided substitutions 51 | */ 52 | registerSubstitutions: (substitutions: LanguageEntry) => vscode.Disposable, 53 | } -------------------------------------------------------------------------------- /src/regexp-iteration.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface MatchResult { 4 | start: number, 5 | end: number, 6 | matchStart: number, 7 | matchEnd: number, 8 | id: number, 9 | } 10 | 11 | /** 12 | * Iterates through each match-group that occurs in the `str`; note that the offset within the given string increments according with the length of the matched group, effectively treating any other portion of the matched expression as a "pre" or "post" match that do not contribute toward advancing through the string. 13 | * The iterator's `next' method accepts a new offset to jump to within the string. 14 | */ 15 | export function *iterateMatches(str: string, re: RegExp, start?: number) : IterableIterator { 16 | re.lastIndex = start===undefined ? 0 : start; 17 | let match : RegExpExecArray; 18 | while((match = re.exec(str))) { 19 | if(match.length <= 1) 20 | return; 21 | const validMatches = match 22 | .map((value,idx) => ({index:idx,match:value})) 23 | .filter((value) => value.match !== undefined); 24 | if(validMatches.length > 1) { 25 | const matchIdx = validMatches[validMatches.length-1].index; 26 | const matchStr = match[matchIdx]; 27 | const matchStart = match.index; 28 | const matchEnd = matchStart + match[0].length; 29 | const start = matchStart + match[0].indexOf(matchStr); 30 | const end = start + matchStr.length; 31 | 32 | const newOffset = yield {start: start, end: end, matchStart: matchStart, matchEnd: matchEnd, id: matchIdx-1}; 33 | if(typeof newOffset === 'number') 34 | re.lastIndex = Math.max(0,Math.min(str.length,newOffset)); 35 | else 36 | re.lastIndex = end; 37 | } 38 | } 39 | } 40 | 41 | export function *iterateMatchArray(str: string, res: RegExp[], start?: number) : IterableIterator { 42 | start = start===undefined ? 0 : start; 43 | res.forEach(re => re.lastIndex = start); 44 | let matches = res.map(re => re.exec(str)); 45 | let matchIdx = matches.findIndex(m => m && m.length > 1); 46 | while(matchIdx >= 0) { 47 | const match = matches[matchIdx]; 48 | const matchStr = match[1]; 49 | const matchStart = match.index; 50 | const matchEnd = matchStart + match[0].length; 51 | const start = matchStart + match[0].indexOf(matchStr); 52 | const end = start + matchStr.length; 53 | 54 | const newOffset = yield {start: start, end: end, matchStart: matchStart, matchEnd: matchEnd, id: matchIdx}; 55 | if(typeof newOffset === 'number') { 56 | const next = Math.max(0,Math.min(str.length,newOffset)); 57 | res.forEach(re => re.lastIndex = next) 58 | } else 59 | res.forEach(re => re.lastIndex = end) 60 | matches = res.map(re => re.exec(str)); 61 | matchIdx = matches.findIndex(m => m && m.length > 1); 62 | } 63 | } 64 | 65 | export function *mapIterator(iter: IterableIterator, f: (x:T1)=>T2, current?: IteratorResult) : IterableIterator { 66 | if(!current) 67 | current = iter.next(); 68 | while(!current.done) { 69 | current = iter.next(yield f(current.value)); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/position.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | //import {Substitution} from './configuration'; 3 | 4 | export function adjustCursorMovement(start: vscode.Position, end: vscode.Position, doc: vscode.TextDocument, avoidRanges: vscode.Range[]) : {pos: vscode.Position, range: vscode.Range} { 5 | try { 6 | const match = findInSortedRanges(end, avoidRanges, {excludeStart: true, excludeEnd: true}); 7 | if(match) { 8 | if(start.line==end.line && start.character > end.character) 9 | return {pos: match.range.start, range: match.range } // moving left 10 | else if(start.line==end.line && start.character < end.character) 11 | return {pos: match.range.end, range: match.range }; // moving right 12 | } 13 | } catch(e) { 14 | console.log(e); 15 | } 16 | return {pos: end, range: undefined}; 17 | } 18 | 19 | // function findClosestInPrettyDecorations(pos: vscode.Position, prettySubsts: PrettySubstitution[], options: {excludeStart?: boolean, excludeEnd?: boolean} = {excludeStart: false, excludeEnd: false}) { 20 | // for(let prettyIdx = 0; prettyIdx < prettySubsts.length; ++prettyIdx) { 21 | // const subst = prettySubsts[prettyIdx]; 22 | // let match = findClosestInSortedRanges(pos,subst.preRanges,options); 23 | // if(match) 24 | // return {range:match.range,index:match.index,prettyIndex:prettyIdx,pre: true}; 25 | // match = findClosestInSortedRanges(pos,subst.postRanges,options); 26 | // if(match) 27 | // return {range:match.range,index:match.index,prettyIndex:prettyIdx,pre:false}; 28 | // } 29 | // return undefined; 30 | // } 31 | 32 | function findInSortedRanges(pos: vscode.Position, ranges: vscode.Range[], options: {excludeStart?: boolean, excludeEnd?: boolean} = {excludeStart: false, excludeEnd: false}) : {range:vscode.Range,index:number} { 33 | const exclStart = options.excludeStart || false; 34 | const exclEnd = options.excludeEnd || false; 35 | let begin = 0; 36 | let end = ranges.length; 37 | while(begin < end) { 38 | const idx = Math.floor((begin + end)/2); 39 | const range = ranges[idx]; 40 | if(range.contains(pos) && !(exclStart && range.start.isEqual(pos)) && !(exclEnd && range.end.isEqual(pos))) 41 | return {range: range, index: idx}; 42 | else if(pos.isBefore(range.start)) 43 | end = idx; 44 | else 45 | begin = idx+1; 46 | } 47 | return undefined; 48 | } 49 | 50 | /* function findClosestInSortedRanges(pos: vscode.Position, ranges: vscode.Range[], options: {excludeStart?: boolean, excludeEnd?: boolean} = {excludeStart: false, excludeEnd: false}) : {range:vscode.Range,index:number} { 51 | const exclStart = options.excludeStart || false; 52 | const exclEnd = options.excludeEnd || false; 53 | let begin = 0; 54 | let end = ranges.length; 55 | while(begin < end) { 56 | const idx = Math.floor((begin + end)/2); 57 | const range = ranges[idx]; 58 | if(range.contains(pos) && !(exclStart && range.start.isEqual(pos)) && !(exclEnd && range.end.isEqual(pos))) 59 | return {range: range, index: idx}; 60 | else if(pos.isBefore(range.start)) 61 | end = idx; 62 | else 63 | begin = idx+1; 64 | } 65 | 66 | for(let idx = begin; idx < ranges.length; ++idx) { 67 | const range = ranges[idx]; 68 | if(range.start.isAfterOrEqual(pos) && !(exclStart && range.start.isEqual(pos))) 69 | return {range: range, index: idx}; 70 | } 71 | return undefined; 72 | } */ -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** Describes conditions in which a symbol may be temporarily revealed */ 4 | export type UglyRevelation = 5 | 'cursor' // the cursor reveals any ugly symbol it touches 6 | | 'cursor-inside' // the cursor reveals any symbol it enters 7 | | 'active-line' // the cursor reveals all symbols on the same line 8 | | 'selection' // the cursor reveals all symbols within a selection 9 | | 'none'; // the cursor does not reveal any symbol 10 | 11 | /** Controls how a symbol is rendered when a cursor is on it */ 12 | export type PrettyCursor = 13 | 'boxed' // render an outline around the symbol 14 | | 'none' // do change to the symbol 15 | 16 | /** Essentially mirrors vscode.DecorationRenderOptions, but restricted to the 17 | * properties that apply to both :before/:after decorations and plain decorations */ 18 | export interface ConcealStyleProperties { 19 | border?: string, 20 | textDecoration?: string, 21 | color?: string | vscode.ThemeColor, 22 | backgroundColor?: string | vscode.ThemeColor, 23 | hackCSS?: string, 24 | } 25 | export interface ConcealStyle extends ConcealStyleProperties { 26 | dark?: ConcealStyleProperties, 27 | light?: ConcealStyleProperties, 28 | } 29 | 30 | /** Copy all defined stying properties to the target */ 31 | export function assignStyleProperties(target: ConcealStyleProperties, source: ConcealStyleProperties) { 32 | if(target===undefined || source===undefined) 33 | return; 34 | if(source.backgroundColor) 35 | target.backgroundColor = source.backgroundColor; 36 | if(source.border) 37 | target.border = source.border; 38 | if(source.color) 39 | target.color = source.color; 40 | if(source.textDecoration) { 41 | target.textDecoration = source.textDecoration + (source.hackCSS ? '; ' + source.hackCSS : ""); 42 | } else if(source.hackCSS) 43 | target.textDecoration = 'none; ' + source.hackCSS; 44 | } 45 | 46 | /** An individual substitution */ 47 | export interface Substitution { 48 | ugly: string, // regular expression describing the text to replace 49 | pretty: string, // plain-text symbol to show instead 50 | pre?: string, // regular expression guard on text before "ugly" 51 | post?: string, // regular expression guard on text after "ugly" 52 | style?: ConcealStyle, // stylings to apply to the "pretty" text, if specified, or else the ugly text 53 | scope?: string, // TextMate scope; if specified, the ugly portion's range must be exactly within the given scope 54 | } 55 | 56 | /** The substitution settings for a language (or group of languages) */ 57 | export interface LanguageEntry { 58 | /** language(s) to apply these substitutions on */ 59 | language: vscode.DocumentSelector; 60 | /** substitution rules */ 61 | substitutions: Substitution[]; 62 | /** If `true`, combine adjacent tokens if they have the same scope name */ 63 | combineIdenticalScopes: boolean; 64 | /** try to make pretty-symbol act like a single character */ 65 | adjustCursorMovement?: boolean; 66 | /** when to unfold a symbol to reveal its underlying text; in response to cursors or selections */ 67 | revealOn?: UglyRevelation; 68 | /** rendering tweaks to a symbol when in proximity to the cursor */ 69 | prettyCursor?: PrettyCursor; 70 | /** The filename of the language's grammar. If specified, substitutions' scope entries refer to scope names in the given grammar. If undefined, then search for a grammar.*/ 71 | textMateGrammar?: string; 72 | /** Override the TextMate scope for the language */ 73 | textMateInitialScope?: string; 74 | } 75 | 76 | export type HideTextMethod = "hack-fontSize" | "hack-letterSpacing" | "none"; 77 | 78 | /** The settings for `prettifySymbolsMode.substitutions` */ 79 | export interface Settings { 80 | /** Main substitution settings, each targetting a different set of languages. 81 | * If more than one entry matches a document, we will pick the one with the 82 | * highest match-score, as determined by vscode.languages.match() 83 | */ 84 | substitutions: LanguageEntry[], 85 | // Defaults loaded from the top-level settings; applied to language entries that do not specify each property */ 86 | /** try to make pretty-symbol act like a single character */ 87 | adjustCursorMovement: boolean, 88 | /** when to unfold a symbol to reveal its underlying text; in response to cursors or selections */ 89 | revealOn: UglyRevelation, 90 | /** rendering tweaks to a symbol when in proximity to the cursor */ 91 | prettyCursor: PrettyCursor, 92 | /** */ 93 | hideTextMethod: HideTextMethod, 94 | } -------------------------------------------------------------------------------- /test/suite/PrettyModel.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 | // import * as vscode from './vscode-shunt'; 10 | // const pm = proxyquire('../src/PrettyModel', {'vscode': {extname: function(file){return './vscode-shunt'}, '@global': true}}); 11 | import * as vscode from 'vscode'; 12 | import * as pm from '../../src/PrettyModel'; 13 | import * as textUtil from '../../src/text-util'; 14 | 15 | class MockDocumentModel { 16 | constructor(public lines: string[]) {} 17 | public getText(range:vscode.Range) { 18 | const text = this.lines.join(''); 19 | const start = textUtil.offsetAt(text, range.start); 20 | const end = textUtil.offsetAt(text, range.end); 21 | return text.slice(start,end); 22 | } 23 | public getLine(line:number) { 24 | return this.lines[line]; 25 | } 26 | public getLineRange(line:number) { 27 | return new vscode.Range(line,0,line,this.lines[line].length); 28 | } 29 | public getLineCount() { 30 | return this.lines.length 31 | } 32 | public validatePosition(p: vscode.Position) { 33 | const line = Math.max(0,Math.min(this.lines.length-1, p.line)); 34 | const character = Math.max(0,Math.min(this.lines[line].length, p.character)); 35 | return new vscode.Position(line,character); 36 | } 37 | public validateRange(r: vscode.Range) { 38 | return new vscode.Range(this.validatePosition(r.start), this.validatePosition(r.end)) 39 | } 40 | } 41 | 42 | // Defines a Mocha test suite to group tests of similar kind together 43 | suite("PrettyModel", () => { 44 | const langFun = { 45 | language: "plaintext", 46 | substitutions: [{ugly: "fun", pretty: "λ"}], 47 | combineIdenticalScopes: false, 48 | } 49 | 50 | function assertDecs(actual: pm.UpdateDecorationEntry[], expected: vscode.Range[][]) { 51 | assert.equal(actual.length, expected.length); 52 | actual.forEach((a,idx) => assert.deepStrictEqual(a.ranges, expected[idx])) 53 | } 54 | 55 | function range(a,b,c,d) { 56 | return new vscode.Range(a,b,c,d); 57 | } 58 | 59 | test("new", () => { 60 | const doc = new MockDocumentModel(["aa", "_fun"]); 61 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 62 | assertDecs(m.getDecorationsList(), [[range(1,1,1,4)], [range(1,1,1,4)]]) 63 | }); 64 | 65 | test("reparsePretties", () => { 66 | const doc = new MockDocumentModel(["aa\r\n", "_fun\r\n"]); 67 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 68 | assertDecs(m.getDecorationsList(), [[range(1,1,1,4)], [range(1,1,1,4)]]) 69 | doc.lines = ["_fun\r\n", "aa\r\n"]; 70 | m['reparsePretties'](range(0,0,1,4)); 71 | assertDecs(m.getDecorationsList(), [[range(0,1,0,4)], [range(0,1,0,4)]]) 72 | }); 73 | 74 | test("applyChanges - in parts", () => { 75 | const doc = new MockDocumentModel(["aa\r\n", "_fun\r\n"]); 76 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 77 | doc.lines = ["aa\r\n"]; 78 | m.applyChanges([{range: range(0,2,1,4), text: ""}]); 79 | assertDecs(m.getDecorationsList(), [[], []]) 80 | doc.lines = ["_fun\r\n", "aa\r\n"]; 81 | m.applyChanges([{range: range(0,0,0,0), text: "_fun"}]); 82 | assertDecs(m.getDecorationsList(), [[range(0,1,0,4)], [range(0,1,0,4)]]) 83 | }); 84 | 85 | test("applyChanges - in sum", () => { 86 | const doc = new MockDocumentModel(["aa\r\n", "_fun\r\n"]); 87 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 88 | doc.lines = ["_fun\r\n", "aa\r\n"]; 89 | m.applyChanges([{range: range(0,2,1,4), text: ""}, {range: range(0,0,0,0), text: "_fun"}]); 90 | assertDecs(m.getDecorationsList(), [[range(0,1,0,4)], [range(0,1,0,4)]]) 91 | }); 92 | 93 | test("getDecoratedText0", () => { 94 | const doc = new MockDocumentModel(["fun\r\n"]); 95 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 96 | assert.equal(m.getDecoratedText(range(0,0,0,3)), "λ") 97 | }); 98 | 99 | test("getDecoratedText1", () => { 100 | const doc = new MockDocumentModel(["aa\r\n", "_fun\r\n"]); 101 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 102 | assert.equal(m.getDecoratedText(range(0,0,1,4)), "aa\r\n_λ") 103 | }); 104 | 105 | test("getDecoratedText2", () => { 106 | const doc = new MockDocumentModel(["aa\r\n", "_fun\r\n", "asdf fun as fun asd"]); 107 | const m = new pm.PrettyModel(doc, langFun, {hideTextMethod: "hack-fontSize"}) 108 | assert.equal(m.getDecoratedText(range(0,0,1,4)), "aa\r\n_λ") 109 | assert.equal(m.getDecoratedText(range(0,0,2,0)), "aa\r\n_λ\r\n") 110 | assert.equal(m.getDecoratedText(range(0,0,2,7)), "aa\r\n_λ\r\nasdf fu") 111 | assert.equal(m.getDecoratedText(range(0,0,2,9)), "aa\r\n_λ\r\nasdf λ ") 112 | assert.equal(m.getDecoratedText(range(0,0,2,19)), "aa\r\n_λ\r\nasdf λ as λ asd") 113 | }); 114 | 115 | }); -------------------------------------------------------------------------------- /test/suite/vscode-shunt.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Position { 3 | public line: number; 4 | public character: number; 5 | public constructor(line: number, character: number) { 6 | this.line = line; 7 | this.character = character; 8 | } 9 | 10 | public compareTo(x: Position) : number { 11 | if(this.isBefore(x)) 12 | return -1; 13 | else if(this.isEqual(x)) 14 | return 0; 15 | else 16 | return 1; 17 | } 18 | 19 | public translate(lineDelta?: number, characterDelta?: number): Position; 20 | public translate(change: { lineDelta?: number; characterDelta?: number; }): Position; 21 | public translate(arg1?, arg2?) : Position { 22 | if(typeof arg1 === 'object') { 23 | const change = arg1 as { lineDelta?: number; characterDelta?: number; }; 24 | return new Position(this.line+(change.lineDelta||0), this.character+(change.characterDelta||0)); 25 | } 26 | else { 27 | const lineDelta = arg1 as number; 28 | const characterDelta = arg2 as number; 29 | return new Position(this.line+lineDelta, this.character+characterDelta); 30 | } 31 | } 32 | 33 | public isEqual(x: Position) : boolean { 34 | return this.line === x.line && this.character === x.character; 35 | } 36 | 37 | public isBefore(x: Position) : boolean { 38 | return this.line < x.line || (this.line === x.line && this.character < x.character); 39 | } 40 | 41 | public isAfter(x: Position) : boolean { 42 | return this.line > x.line || (this.line === x.line && this.character > x.character); 43 | } 44 | 45 | public isBeforeOrEqual(x: Position) : boolean { 46 | return this.isBefore(x) || this.isEqual(x); 47 | } 48 | 49 | public isAfterOrEqual(x: Position) : boolean { 50 | return this.isAfter(x) || this.isEqual(x); 51 | } 52 | 53 | public with(line?: number, character?: number): Position; 54 | public with(change: { line?: number; character?: number; }): Position; 55 | public with(arg1?, arg2?) { 56 | if(typeof arg1 === 'object') { 57 | const change = arg1 as { line?: number; character?: number; }; 58 | return new Position(change.line===undefined ? this.line : change.line as number, change.character===undefined ? this.character : change.character as number) 59 | } else { 60 | return new Position(arg1===undefined ? this.line : arg1 as number, arg2===undefined ? this.character : arg2 as number) 61 | } 62 | } 63 | 64 | } 65 | 66 | export class Range { 67 | public readonly start: Position; 68 | public readonly end: Position; 69 | public readonly isEmpty : boolean; 70 | public readonly isSingleLine : boolean; 71 | public constructor(start: Position, end: Position); 72 | public constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number); 73 | public constructor(arg1: number|Position, arg2: number|Position, arg3?: number, arg4?: number) { 74 | if(typeof arg1 === 'number') { 75 | this.start = new Position(arg1, arg2 as number); 76 | this.end = new Position(arg3 as number, arg4 as number); 77 | } else { 78 | this.start = arg1; 79 | this.end = arg2 as Position; 80 | } 81 | this.isEmpty = this.start.isEqual(this.end); 82 | this.isSingleLine = this.start.line === this.end.line; 83 | } 84 | 85 | public contains(positionOrRange: Position | Range): boolean { 86 | if(Object.hasOwnProperty.call(positionOrRange, 'line')) 87 | return this.start.isBeforeOrEqual(positionOrRange as Position) && this.end.isAfter(positionOrRange as Position); 88 | else 89 | return this.start.isBeforeOrEqual((positionOrRange as Range).start) && this.end.isAfterOrEqual((positionOrRange as Range).end); 90 | } 91 | 92 | public isEqual(other: Range): boolean { 93 | return this.start.isEqual(other.start) && this.end.isEqual(other.end); 94 | } 95 | 96 | public intersection(range: Range): Range { 97 | if(this.start.isBeforeOrEqual(range.start)) { 98 | if(this.end.isAfterOrEqual(range.end)) 99 | return new Range(range.start,range.end); 100 | else if(this.end.isAfterOrEqual(range.start)) 101 | return new Range(range.start,this.end); 102 | else 103 | return undefined; 104 | } else 105 | return range.intersection(this) 106 | } 107 | 108 | public union(other: Range): Range { 109 | return new Range(this.start.isBefore(other.start) ? this.start : other.start, this.end.isAfter(other.end) ? this.end : other.end); 110 | } 111 | 112 | /** 113 | * Derived a new range from this range. 114 | * 115 | * @param start A position that should be used as start. The default value is the [current start](#Range.start). 116 | * @param end A position that should be used as end. The default value is the [current end](#Range.end). 117 | * @return A range derived from this range with the given start and end position. 118 | * If start and end are not different `this` range will be returned. 119 | */ 120 | public with(start?: Position, end?: Position): Range; 121 | public with(change: { start?: Position, end?: Position }): Range; 122 | public with(arg1?, arg2?) : Range { 123 | if(arg1===undefined || arg1.hasOwnPropert('line')) 124 | return new Range(arg1===undefined ? this.start : arg1 as Position, arg2===undefined ? this.end : arg2 as Position) 125 | else { 126 | const change = arg1 as { start?: Position, end?: Position }; 127 | return new Range(change.start || this.start, change.end || this.end); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /src/text-mate.ts: -------------------------------------------------------------------------------- 1 | //import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | const tm = loadTextMate(); 4 | 5 | // From https://github.com/siegebell/scope-info/issues/5 6 | function getNodeModule(moduleName) { 7 | try { 8 | console.log(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`) 9 | return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`); 10 | } catch(err) { 11 | console.log(err); 12 | } 13 | try { 14 | console.log(`>>> ${vscode.env.appRoot}/node_modules/${moduleName}`) 15 | return require(`${vscode.env.appRoot}/node_modules/${moduleName}`); 16 | } catch(err) { 17 | console.log(err); 18 | } 19 | return null; 20 | } 21 | 22 | function loadTextMate() { 23 | return getNodeModule('vscode-textmate') 24 | } 25 | 26 | // namespace N { 27 | // /** 28 | // * The registry that will hold all grammars. 29 | // */ 30 | // export declare class Registry { 31 | // private readonly _locator; 32 | // private readonly _syncRegistry; 33 | // constructor(locator?: IGrammarLocator); 34 | // /** 35 | // * Load the grammar for `scopeName` and all referenced included grammars asynchronously. 36 | // */ 37 | // loadGrammar(initialScopeName: string, callback: (err: any, grammar: IGrammar) => void): void; 38 | // /** 39 | // * Load the grammar at `path` synchronously. 40 | // */ 41 | // loadGrammarFromPathSync(path: string): IGrammar; 42 | // /** 43 | // * Get the grammar for `scopeName`. The grammar must first be created via `loadGrammar` or `loadGrammarFromPathSync`. 44 | // */ 45 | // grammarForScopeName(scopeName: string): IGrammar; 46 | // } 47 | // } 48 | 49 | export function matchScope(scope: string, scopes: string[]) : boolean { 50 | if(!scope) 51 | return true; 52 | const parts = scope.split(/\s+/); 53 | let idx = 0; 54 | for(const part of parts) { 55 | while(idx < scopes.length && !scopes[idx].startsWith(part)) 56 | ++idx; 57 | if(idx >= scopes.length) 58 | return false; 59 | ++idx; 60 | } 61 | return true; 62 | } 63 | 64 | export interface Registry { 65 | new (locator?: IGrammarLocator); 66 | /** 67 | * Load the grammar for `scopeName` and all referenced included grammars asynchronously. 68 | */ 69 | loadGrammar(initialScopeName: string, callback: (err, grammar: IGrammar) => void): void; 70 | /** 71 | * Load the grammar at `path` synchronously. 72 | */ 73 | loadGrammarFromPathSync(path: string): IGrammar; 74 | /** 75 | * Get the grammar for `scopeName`. The grammar must first be created via `loadGrammar` or `loadGrammarFromPathSync`. 76 | */ 77 | grammarForScopeName(scopeName: string): IGrammar; 78 | } 79 | 80 | const dummyGrammar: IGrammar = { 81 | tokenizeLine(lineText: string, prevState: StackElement): ITokenizeLineResult { 82 | return { 83 | tokens: [], 84 | ruleStack: prevState, 85 | } 86 | } 87 | } 88 | 89 | class DummyRegistry { 90 | public constructor() {} 91 | loadGrammar(initialScopeName: string, callback: (err, grammar: IGrammar) => void) { 92 | callback(new Error("textmate cannot be loaded"), undefined); 93 | } 94 | loadGrammarFromPathSync(): IGrammar { 95 | return dummyGrammar; 96 | } 97 | grammarForScopeName(): IGrammar { 98 | return dummyGrammar; 99 | } 100 | } 101 | 102 | export const Registry : Registry = tm == null ? (DummyRegistry) : tm.Registry; 103 | 104 | /** 105 | * A registry helper that can locate grammar file paths given scope names. 106 | */ 107 | export interface IGrammarLocator { 108 | getFilePath(scopeName: string): string; 109 | getInjections?(scopeName: string): string[]; 110 | } 111 | 112 | export interface IGrammarInfo { 113 | readonly fileTypes: string[]; 114 | readonly name: string; 115 | readonly scopeName: string; 116 | readonly firstLineMatch: string; 117 | } 118 | /** 119 | * A grammar 120 | */ 121 | export interface IGrammar { 122 | /** 123 | * Tokenize `lineText` using previous line state `prevState`. 124 | */ 125 | tokenizeLine(lineText: string, prevState: StackElement): ITokenizeLineResult; 126 | } 127 | export interface ITokenizeLineResult { 128 | readonly tokens: IToken[]; 129 | /** 130 | * The `prevState` to be passed on to the next line tokenization. 131 | */ 132 | readonly ruleStack: StackElement; 133 | } 134 | export interface IToken { 135 | startIndex: number; 136 | readonly endIndex: number; 137 | readonly scopes: string[]; 138 | } 139 | /** 140 | * **IMPORTANT** - Immutable! 141 | */ 142 | export interface StackElement { 143 | _stackElementBrand: void; 144 | readonly _parent: StackElement; 145 | equals(other: StackElement): boolean; 146 | } 147 | 148 | 149 | export function combineIdenticalTokenScopes(tokens: IToken[]) : IToken[] { 150 | if(!tokens || tokens.length === 0) 151 | return []; 152 | const result = [tokens[0]]; 153 | let prevToken = tokens[0]; 154 | for(let idx = 1; idx < tokens.length; ++idx) { 155 | const token = tokens[idx]; 156 | if(prevToken.endIndex===token.startIndex && token.scopes.length === prevToken.scopes.length && token.scopes.every((t,idx) => t === prevToken.scopes[idx])) { 157 | // Note: create a copy of the object so the source tokens are unmodified 158 | result[result.length-1] = {startIndex: prevToken.startIndex, endIndex: token.endIndex, scopes: prevToken.scopes} 159 | prevToken = result[result.length-1]; 160 | } else { 161 | result.push(token); 162 | prevToken = token; 163 | } 164 | } 165 | return result; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /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 | export function offsetAt(text: string, pos: vscode.Position) : number { 15 | let line = pos.line; 16 | let lastIndex = 0; 17 | while (line > 0) { 18 | const match = lineEndingRE.exec(text.substring(lastIndex)); 19 | if(match[2] === '' || match[2] === undefined) // no line-ending found 20 | return -1; // the position is beyond the length of text 21 | else { 22 | lastIndex+= match[0].length; 23 | --line; 24 | } 25 | } 26 | return lastIndex + pos.character; 27 | } 28 | 29 | /** Calculates the offset into text of pos, where textStart is the position where text starts and both pos and textStart are absolute positions 30 | * @return the offset into text indicated by pos, or -1 if pos is out of range 31 | * 32 | * 'abc\ndef' 33 | * 'acbX\ndef' 34 | * +++*** --> +++_*** 35 | * */ 36 | export function relativeOffsetAtAbsolutePosition(text: string, textStart: vscode.Position, pos: vscode.Position) : number { 37 | let line = textStart.line; 38 | let currentOffset = 0; 39 | // count the relative lines and offset w.r.t text 40 | while(line < pos.line) { 41 | const match = lineEndingRE.exec(text.substring(currentOffset)); 42 | ++line; // there was a new line 43 | currentOffset += match[0].length; 44 | } 45 | 46 | if(line > pos.line) 47 | return -1 48 | else if(textStart.line === pos.line) 49 | return Math.max(-1, pos.character - textStart.character); 50 | else // if(line === pos.line) 51 | return Math.max(-1, pos.character + currentOffset); 52 | } 53 | 54 | /** 55 | * @returns the Position (line, column) for the location (character position), assuming that text begins at start 56 | */ 57 | export function positionAtRelative(start: vscode.Position, text: string, offset: number) : vscode.Position { 58 | if(offset > text.length) 59 | offset = text.length; 60 | let line = start.line; 61 | let currentOffset = 0; // offset into text we are current at; <= `offset` 62 | let lineOffset = start.character; 63 | //let lastIndex = start.character; 64 | let found = false; 65 | let pos = null; 66 | while(!found) { 67 | const match = lineEndingRE.exec(text.substring(currentOffset)); 68 | // match[0] -- characters plus newline 69 | // match[1] -- characters up to newline 70 | // match[2] -- newline (\n, \r, or \r\n) 71 | if(!match || match[0].length === 0 || currentOffset + match[1].length >= offset) { 72 | found = true; 73 | pos = new vscode.Position(line, lineOffset + Math.max(offset - currentOffset, 0)) 74 | } 75 | currentOffset+= match[0].length; 76 | lineOffset = 0; 77 | ++line; 78 | } 79 | return pos; 80 | } 81 | 82 | 83 | /** 84 | * @returns the Position (line, column) for the location (character position) 85 | */ 86 | export function positionAt(text: string, offset: number) : vscode.Position { 87 | if(offset > text.length) 88 | offset = text.length; 89 | let line = 0; 90 | let lastIndex = 0; 91 | let found = false; 92 | let pos = null; 93 | while(!found) { 94 | const match = lineEndingRE.exec(text.substring(lastIndex)); 95 | if(lastIndex + match[1].length >= offset) { 96 | found = true; 97 | pos = new vscode.Position(line, Math.max(0, offset - lastIndex)); 98 | } 99 | lastIndex+= match[0].length; 100 | ++line; 101 | } 102 | return pos; 103 | } 104 | 105 | /** 106 | * @returns the lines and characters represented by the text 107 | */ 108 | export function toRangeDelta(oldRange:vscode.Range, text: string) : RangeDelta { 109 | const newEnd = positionAt(text,text.length); 110 | let charsDelta; 111 | if(oldRange.start.line == oldRange.end.line) 112 | charsDelta = newEnd.character - (oldRange.end.character-oldRange.start.character); 113 | else 114 | charsDelta = newEnd.character - oldRange.end.character; 115 | 116 | return { 117 | start: oldRange.start, 118 | end: oldRange.end, 119 | linesDelta: newEnd.line-(oldRange.end.line-oldRange.start.line), 120 | endCharactersDelta: charsDelta 121 | }; 122 | } 123 | 124 | export function rangeDeltaNewRange(delta: RangeDelta) : vscode.Range { 125 | let x : number; 126 | if (delta.linesDelta > 0) 127 | x = delta.endCharactersDelta; 128 | else if (delta.linesDelta < 0 && delta.start.line == delta.end.line + delta.linesDelta) 129 | x = delta.end.character + delta.endCharactersDelta + delta.start.character; 130 | else 131 | x = delta.end.character + delta.endCharactersDelta; 132 | return new vscode.Range(delta.start, new vscode.Position(delta.end.line + delta.linesDelta, x)); 133 | } 134 | 135 | function positionRangeDeltaTranslate(pos: vscode.Position, delta: RangeDelta) : vscode.Position { 136 | if(pos.isBefore(delta.end)) 137 | return pos; 138 | else if (delta.end.line == pos.line) { 139 | let x = pos.character + delta.endCharactersDelta; 140 | if (delta.linesDelta > 0) 141 | x = x - delta.end.character; 142 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 143 | x = x + delta.start.character; 144 | return new vscode.Position(pos.line + delta.linesDelta, x); 145 | } 146 | else // if(pos.line > delta.end.line) 147 | return new vscode.Position(pos.line + delta.linesDelta, pos.character); 148 | } 149 | 150 | function positionRangeDeltaTranslateEnd(pos: vscode.Position, delta: RangeDelta) : vscode.Position { 151 | if(pos.isBeforeOrEqual(delta.end)) 152 | return pos; 153 | else if (delta.end.line == pos.line) { 154 | let x = pos.character + delta.endCharactersDelta; 155 | if (delta.linesDelta > 0) 156 | x = x - delta.end.character; 157 | else if (delta.start.line == delta.end.line + delta.linesDelta && delta.linesDelta < 0) 158 | x = x + delta.start.character; 159 | return new vscode.Position(pos.line + delta.linesDelta, x); 160 | } 161 | else // if(pos.line > delta.end.line) 162 | return new vscode.Position(pos.line + delta.linesDelta, pos.character); 163 | } 164 | 165 | export function rangeTranslate(range: vscode.Range, delta: RangeDelta) { 166 | return new vscode.Range( 167 | positionRangeDeltaTranslate(range.start, delta), 168 | positionRangeDeltaTranslateEnd(range.end, delta) 169 | ) 170 | } 171 | 172 | export function rangeContains(range: vscode.Range, pos: vscode.Position, exclStart=false, inclEnd=false) { 173 | return range.start.isBeforeOrEqual(pos) 174 | && (!exclStart || !range.start.isEqual(pos)) 175 | && ((inclEnd && range.end.isEqual(pos)) || range.end.isAfter(pos)); 176 | } 177 | 178 | export function maxPosition(x: vscode.Position, y: vscode.Position) { 179 | if(x.line < y.line) 180 | return x; 181 | if(x.line < x.line) 182 | return y; 183 | if(x.character < y.character) 184 | return x; 185 | else 186 | return y; 187 | } 188 | -------------------------------------------------------------------------------- /src/decorations.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import {Substitution, assignStyleProperties, } from './configuration'; 3 | 4 | export function makePrettyDecoration_fontSize_hack(prettySubst: Substitution) { 5 | const showAttachmentStyling = ''; 6 | 7 | const styling : vscode.DecorationRenderOptions = { 8 | after: {}, 9 | dark: {after: {}}, 10 | light: {after: {}}, 11 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 12 | }; 13 | if(prettySubst.style) { 14 | assignStyleProperties(styling.after, prettySubst.style); 15 | if(prettySubst.style.dark) 16 | assignStyleProperties(styling.dark.after, prettySubst.style.dark); 17 | if(prettySubst.style.light) 18 | assignStyleProperties(styling.light.after, prettySubst.style.light); 19 | } 20 | styling.after.contentText = prettySubst.pretty; 21 | 22 | // Use a dirty hack to change the font size (code injection) 23 | styling.after.textDecoration = (styling.after.textDecoration || 'none') + showAttachmentStyling; 24 | // and make sure the user's textDecoration does not break our hack 25 | if(styling.light.after.textDecoration) 26 | styling.light.after.textDecoration = styling.light.after.textDecoration + showAttachmentStyling; 27 | if(styling.dark.after.textDecoration) 28 | styling.dark.after.textDecoration = styling.dark.after.textDecoration + showAttachmentStyling; 29 | 30 | return vscode.window.createTextEditorDecorationType(styling); 31 | } 32 | 33 | export function makePrettyDecoration_letterSpacing_hack(prettySubst: Substitution) { 34 | // const showAttachmentStyling = '; font-size: 10em; letter-spacing: normal; visibility: visible'; 35 | const showAttachmentStyling = '; letter-spacing: normal; visibility: visible'; 36 | 37 | const styling : vscode.DecorationRenderOptions = { 38 | after: {}, 39 | dark: {after: {}}, 40 | light: {after: {}}, 41 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 42 | }; 43 | if(prettySubst.style) { 44 | assignStyleProperties(styling.after, prettySubst.style); 45 | if(prettySubst.style.dark) 46 | assignStyleProperties(styling.dark.after, prettySubst.style.dark); 47 | if(prettySubst.style.light) 48 | assignStyleProperties(styling.light.after, prettySubst.style.light); 49 | } 50 | styling.after.contentText = prettySubst.pretty; 51 | 52 | // Use a dirty hack to change the font size (code injection) 53 | styling.after.textDecoration = (styling.after.textDecoration || 'none') + showAttachmentStyling; 54 | // and make sure the user's textDecoration does not break our hack 55 | if(styling.light.after.textDecoration) 56 | styling.light.after.textDecoration = styling.light.after.textDecoration + showAttachmentStyling; 57 | if(styling.dark.after.textDecoration) 58 | styling.dark.after.textDecoration = styling.dark.after.textDecoration + showAttachmentStyling; 59 | 60 | return vscode.window.createTextEditorDecorationType(styling); 61 | } 62 | 63 | export function makePrettyDecoration_noPretty(prettySubst: Substitution) { 64 | //const showAttachmentStyling = ''; 65 | 66 | const styling : vscode.DecorationRenderOptions = { 67 | dark: {}, 68 | light: {}, 69 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 70 | }; 71 | if(prettySubst.style) { 72 | assignStyleProperties(styling, prettySubst.style); 73 | if(prettySubst.style.dark) 74 | assignStyleProperties(styling.dark, prettySubst.style.dark); 75 | if(prettySubst.style.light) 76 | assignStyleProperties(styling.light, prettySubst.style.light); 77 | } 78 | 79 | return vscode.window.createTextEditorDecorationType(styling); 80 | } 81 | 82 | export function makePrettyDecoration_noHide(prettySubst: Substitution) { 83 | const showAttachmentStyling = ''; 84 | 85 | const styling : vscode.DecorationRenderOptions = { 86 | after: {}, 87 | dark: {after: {}}, 88 | light: {after: {}}, 89 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 90 | }; 91 | if(prettySubst.style) { 92 | assignStyleProperties(styling.after, prettySubst.style); 93 | if(prettySubst.style.dark) 94 | assignStyleProperties(styling.dark.after, prettySubst.style.dark); 95 | if(prettySubst.style.light) 96 | assignStyleProperties(styling.light.after, prettySubst.style.light); 97 | } 98 | styling.after.contentText = prettySubst.pretty; 99 | 100 | // Use a dirty hack to change the font size (code injection) 101 | styling.after.textDecoration = (styling.after.textDecoration || 'none') + showAttachmentStyling; 102 | // and make sure the user's textDecoration does not break our hack 103 | if(styling.light.after.textDecoration) 104 | styling.light.after.textDecoration = styling.light.after.textDecoration + showAttachmentStyling; 105 | if(styling.dark.after.textDecoration) 106 | styling.dark.after.textDecoration = styling.dark.after.textDecoration + showAttachmentStyling; 107 | 108 | return vscode.window.createTextEditorDecorationType(styling); 109 | } 110 | 111 | 112 | export function makeDecorations_fontSize_hack() { 113 | return { 114 | uglyDecoration: vscode.window.createTextEditorDecorationType({ 115 | textDecoration: 'none; font-size: 0.001em', 116 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 117 | }), 118 | revealedUglyDecoration: vscode.window.createTextEditorDecorationType({ 119 | textDecoration: 'none; font-size: inherit !important', 120 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 121 | after: { 122 | textDecoration: 'none; font-size: 0pt', 123 | } 124 | }), 125 | boxedSymbolDecoration: { 126 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 127 | after: { 128 | border: '0.1em solid', 129 | margin: '-0em -0.05em -0em -0.1em', 130 | } 131 | }, 132 | } 133 | } 134 | 135 | export function makeDecorations_letterSpacing_hack() { 136 | return { 137 | uglyDecoration: vscode.window.createTextEditorDecorationType({ 138 | letterSpacing: "-0.55em; font-size: 0.1em; visibility: hidden", 139 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 140 | }), 141 | revealedUglyDecoration: vscode.window.createTextEditorDecorationType({ 142 | letterSpacing: "normal !important; font-size: inherit !important; visibility: visible !important", 143 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 144 | after: { 145 | // letterSpacing: '-0.55em; font-size: 0.1pt; visibility: hidden', 146 | textDecoration: 'none !important; font-size: 0.1pt !important; visibility: hidden', 147 | } 148 | }), 149 | boxedSymbolDecoration: { 150 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 151 | after: { 152 | border: '0.1em solid', 153 | margin: '-0em -0.05em -0em -0.1em', 154 | } 155 | }, 156 | } 157 | } 158 | 159 | export function makeDecorations_none() { 160 | return { 161 | uglyDecoration: vscode.window.createTextEditorDecorationType({ 162 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 163 | }), 164 | revealedUglyDecoration: vscode.window.createTextEditorDecorationType({ 165 | textDecoration: 'none; font-size: inherit !important', 166 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 167 | after: { 168 | textDecoration: 'none; font-size: 0pt', 169 | } 170 | }), 171 | boxedSymbolDecoration: { 172 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, 173 | after: { 174 | border: '0.1em solid', 175 | margin: '-0em -0.05em -0em -0.1em', 176 | } 177 | }, 178 | } 179 | 180 | } -------------------------------------------------------------------------------- /test/suite/RangeSet.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 | // const proxyquire = require('proxyquire').noCallThru(); 10 | // import * as vscode from './vscode-shunt'; 11 | // const rs = proxyquire('../src/RangeSet', {'vscode': {extname: function(file){return './vscode-shunt'}, '@global': true}}); 12 | import * as vscode from 'vscode'; 13 | import * as rs from '../../src/RangeSet'; 14 | 15 | // Defines a Mocha test suite to group tests of similar kind together 16 | suite("RangeSet", () => { 17 | 18 | test("add", function() { 19 | const x1 = new rs.RangeSet(); 20 | x1.add(new vscode.Range(1,9,1,14)); 21 | x1.add(new vscode.Range(1,20,1,22)); 22 | x1.add(new vscode.Range(1,11,1,18)); 23 | x1.add(new vscode.Range(0,10,1,1)); 24 | assert.deepStrictEqual(x1.getRanges(), [new vscode.Range(0,10,1,1), new vscode.Range(1,9,1,18), new vscode.Range(1,20,1,22)]); 25 | }) 26 | 27 | test("indexAt", function() { 28 | const x1 = new rs.RangeSet(); 29 | x1.add(new vscode.Range(1,9,1,14)); // 1 30 | x1.add(new vscode.Range(1,16,1,18)); // 2 31 | x1.add(new vscode.Range(0,10,1,1)); // 0 32 | assert.equal(x1['indexAt'](new vscode.Position(0,0)), 0); 33 | assert.equal(x1['indexAt'](new vscode.Position(1,8)), 0); 34 | assert.equal(x1['indexAt'](new vscode.Position(1,5)), 0); 35 | assert.equal(x1['indexAt'](new vscode.Position(1,9)), 1); 36 | assert.equal(x1['indexAt'](new vscode.Position(1,14)), 1); 37 | assert.equal(x1['indexAt'](new vscode.Position(1,15)), 1); 38 | assert.equal(x1['indexAt'](new vscode.Position(1,16)), 2); 39 | assert.equal(x1['indexAt'](new vscode.Position(1,18)), 2); 40 | assert.equal(x1['indexAt'](new vscode.Position(1,19)), 2); 41 | }) 42 | 43 | test("getOverlapping - singleton", () => { 44 | const x1 = new rs.RangeSet(); 45 | const r1 = new vscode.Range(1,9,1,14); 46 | x1.add(r1); 47 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,9,1,9), {includeTouchingStart: true, includeTouchingEnd: true}), [r1]); 48 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,15), {includeTouchingStart: true, includeTouchingEnd: true}), [r1]); 49 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(0,1,1,9), {includeTouchingStart: true, includeTouchingEnd: true}), [r1]); 50 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,15), {includeTouchingStart: true, includeTouchingEnd: false}), [r1]); 51 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,15), {includeTouchingStart: false, includeTouchingEnd: true}), []); 52 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(0,1,1,9), {includeTouchingStart: false, includeTouchingEnd: true}), [r1]); 53 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(0,1,1,9), {includeTouchingStart: true, includeTouchingEnd: false}), []); 54 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,15), {includeTouchingStart: false, includeTouchingEnd: false}), []); 55 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(0,1,1,9), {includeTouchingStart: false, includeTouchingEnd: false}), []); 56 | }); 57 | 58 | test("getOverlapping", () => { 59 | const x1 = new rs.RangeSet(); 60 | const r1 = new vscode.Range(1,9,1,14); 61 | const r2 = new vscode.Range(1,17,1,20); 62 | x1.add(r1); 63 | x1.add(r2); 64 | // pre 65 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,0,1,9), {includeTouchingStart: false, includeTouchingEnd: false}), []); 66 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,0,1,9), {includeTouchingStart: true, includeTouchingEnd: false}), []); 67 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,0,1,9), {includeTouchingStart: false, includeTouchingEnd: true}), [r1]); 68 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,0,1,9), {includeTouchingStart: true, includeTouchingEnd: true}), [r1]); 69 | // middle 70 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,17), {includeTouchingStart: false, includeTouchingEnd: false}), []); 71 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,17), {includeTouchingStart: true, includeTouchingEnd: false}), [r1]); 72 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,17), {includeTouchingStart: false, includeTouchingEnd: true}), [r2]); 73 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,14,1,17), {includeTouchingStart: true, includeTouchingEnd: true}), [r1,r2]); 74 | // end 75 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,20,1,22), {includeTouchingStart: false, includeTouchingEnd: false}), []); 76 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,20,1,22), {includeTouchingStart: true, includeTouchingEnd: false}), [r2]); 77 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,20,1,22), {includeTouchingStart: false, includeTouchingEnd: true}), []); 78 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,20,1,22), {includeTouchingStart: true, includeTouchingEnd: true}), [r2]); 79 | // both 80 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,10,1,21), {includeTouchingStart: false, includeTouchingEnd: false}), [r1,r2]); 81 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,10,1,21), {includeTouchingStart: true, includeTouchingEnd: false}), [r1,r2]); 82 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,10,1,21), {includeTouchingStart: false, includeTouchingEnd: true}), [r1,r2]); 83 | assert.deepStrictEqual(x1.getOverlapping(new vscode.Range(1,10,1,21), {includeTouchingStart: true, includeTouchingEnd: true}), [r1,r2]); 84 | }); 85 | 86 | test("removeOverlapping", () => { 87 | const r1 = new vscode.Range(1,9,1,14); 88 | const r2 = new vscode.Range(1,17,1,20); 89 | function tryRemove(a,b,c,d, options: {includeTouchingStart: boolean, includeTouchingEnd: boolean}, expected, remaining) { 90 | const x1 = new rs.RangeSet(); 91 | x1.add(r1); 92 | x1.add(r2); 93 | const r = x1.removeOverlapping(new vscode.Range(a,b,c,d),options); 94 | assert.deepStrictEqual(r, expected); 95 | assert.deepStrictEqual(x1.getRanges(), remaining); 96 | } 97 | // pre 98 | tryRemove(1,0,1,9, {includeTouchingStart: false, includeTouchingEnd: false}, [], [r1,r2]); 99 | tryRemove(1,0,1,9, {includeTouchingStart: true, includeTouchingEnd: false}, [], [r1,r2]); 100 | tryRemove(1,0,1,9, {includeTouchingStart: false, includeTouchingEnd: true}, [r1], [r2]); 101 | tryRemove(1,0,1,9, {includeTouchingStart: true, includeTouchingEnd: true}, [r1], [r2]); 102 | // middle 103 | tryRemove(1,14,1,17, {includeTouchingStart: false, includeTouchingEnd: false}, [], [r1,r2]); 104 | tryRemove(1,14,1,17, {includeTouchingStart: true, includeTouchingEnd: false}, [r1], [r2]); 105 | tryRemove(1,14,1,17, {includeTouchingStart: false, includeTouchingEnd: true}, [r2], [r1]); 106 | tryRemove(1,14,1,17, {includeTouchingStart: true, includeTouchingEnd: true}, [r1,r2], []); 107 | // end 108 | tryRemove(1,20,1,22, {includeTouchingStart: false, includeTouchingEnd: false}, [], [r1,r2]); 109 | tryRemove(1,20,1,22, {includeTouchingStart: true, includeTouchingEnd: false}, [r2], [r1]); 110 | tryRemove(1,20,1,22, {includeTouchingStart: false, includeTouchingEnd: true}, [], [r1,r2]); 111 | tryRemove(1,20,1,22, {includeTouchingStart: true, includeTouchingEnd: true}, [r2], [r1]); 112 | // both 113 | tryRemove(1,10,1,21, {includeTouchingStart: false, includeTouchingEnd: false}, [r1,r2], []); 114 | tryRemove(1,10,1,21, {includeTouchingStart: true, includeTouchingEnd: false}, [r1,r2], []); 115 | tryRemove(1,10,1,21, {includeTouchingStart: false, includeTouchingEnd: true}, [r1,r2], []); 116 | tryRemove(1,10,1,21, {includeTouchingStart: true, includeTouchingEnd: true}, [r1,r2], []); 117 | }); 118 | 119 | }); -------------------------------------------------------------------------------- /src/RangeSet.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import * as textUtil from './text-util'; 5 | 6 | export class RangeSet { 7 | // sorted by start position; assume none overlap or touch 8 | private ranges: vscode.Range[] = []; 9 | 10 | public clear() : void { 11 | this.ranges = []; 12 | this.ranges[0].start.translate(0,0) 13 | } 14 | 15 | /** Adjust the ranges by the given delta */ 16 | public shiftDelta(delta: textUtil.RangeDelta) { 17 | this.ranges.forEach((r,idx) => { 18 | this.ranges[idx] = textUtil.rangeTranslate(this.ranges[idx], delta); 19 | }) 20 | } 21 | 22 | private getOverlappingIndices(range: vscode.Range, options: {includeTouchingStart:boolean,includeTouchingEnd:boolean}) : {begin: number, end: number} { 23 | if(this.ranges.length <= 0) 24 | return {begin: 0, end: 0}; 25 | let begin = Math.max(0, this.indexAt(range.start)); 26 | if(this.ranges[begin].end.isBefore(range.start)) 27 | ++begin; 28 | else if(!options.includeTouchingStart && this.ranges[begin].end.isEqual(range.start)) 29 | ++begin; 30 | 31 | let end = Math.max(begin, this.indexAt(range.end, begin)); 32 | if(end < this.ranges.length && this.ranges[end].end.isAfter(range.start) && this.ranges[end].start.isBefore(range.end)) 33 | ++end; 34 | else if(end < this.ranges.length && options.includeTouchingEnd && this.ranges[end].start.isEqual(range.end)) 35 | ++end; 36 | else if(end < this.ranges.length && options.includeTouchingStart && this.ranges[end].end.isEqual(range.start)) 37 | ++end; 38 | return {begin: begin, end: end} 39 | } 40 | 41 | /** Calculate the set of ranges that overlap `range` */ 42 | public getOverlapping(range: vscode.Range, options: {includeTouchingStart:boolean,includeTouchingEnd:boolean}) : vscode.Range[] { 43 | const {begin: begin, end: end} = this.getOverlappingIndices(range, options); 44 | return this.ranges.slice(begin, end); 45 | } 46 | 47 | /** Removes all ranges that intersect with `range`, returning the removed elements */ 48 | public removeOverlapping(range: vscode.Range, options: {includeTouchingStart:boolean,includeTouchingEnd:boolean}) : vscode.Range[] { 49 | const {begin: begin, end: end} = this.getOverlappingIndices(range, options); 50 | return this.ranges.splice(begin, end-begin); 51 | } 52 | 53 | public add(range: vscode.Range) { 54 | if(range.isEmpty) 55 | return; 56 | if(this.ranges.length == 0) {// init 57 | this.ranges.push(range); 58 | return; 59 | } 60 | 61 | // find the first intersecting range 62 | const beginPos = this.ranges.findIndex((r) => r.end.isAfterOrEqual(range.start)); 63 | 64 | if(beginPos == -1) {// after pos 65 | this.ranges.push(range); 66 | return; 67 | } 68 | 69 | const r0 = this.ranges[beginPos]; 70 | 71 | if(r0.start.isAfter(range.end)) { 72 | // insert before pos 73 | this.ranges.splice(beginPos,0,range); 74 | return; 75 | } 76 | 77 | // find the range *just after* the last intersecting range 78 | let endPos = this.ranges.findIndex((r) => r.start.isAfter(range.end)); 79 | // assume beginPos <= endPos (because this.ranges is sorted) 80 | 81 | if(endPos == -1) 82 | endPos = this.ranges.length; 83 | 84 | const r1 = this.ranges[endPos-1]; 85 | this.ranges[beginPos] = r0.union(r1.union(range)); 86 | this.ranges.splice(beginPos+1,endPos-beginPos-1); 87 | } 88 | 89 | public subtract(range: vscode.Range) { 90 | if(this.ranges.length == 0 || this.ranges[0].isEmpty || range.isEmpty) { 91 | return; // no intersection 92 | } 93 | 94 | // find the first intersecting range 95 | const beginPos = this.ranges.findIndex((r) => r.end.isAfter(range.start)); 96 | 97 | if(beginPos == -1) 98 | return; // after pos; no intersection 99 | 100 | const r0a = this.ranges[beginPos].start; 101 | let remainder1 : vscode.Range; 102 | if(r0a.isAfterOrEqual(range.end)) 103 | return; // no intersection 104 | else if(r0a.isAfterOrEqual(range.start)) 105 | remainder1 = new vscode.Range(r0a, r0a); 106 | else 107 | remainder1 = new vscode.Range(r0a, range.start); 108 | 109 | // find the range *just after* the last intersecting range 110 | let endPos = this.ranges.findIndex((r) => r.end.isAfter(range.end)); 111 | // assume beginPos <= endPos (because this.ranges is sorted) 112 | 113 | let remainder2 : vscode.Range; 114 | if(endPos == -1) { 115 | endPos = this.ranges.length; 116 | remainder2 = new vscode.Range(r0a,r0a); // empty range 117 | } else if(this.ranges[endPos].start.isAfterOrEqual(range.end)) 118 | remainder2 = new vscode.Range(r0a,r0a); 119 | else { 120 | remainder2 = new vscode.Range(range.end, this.ranges[endPos].end); 121 | ++endPos; // endPos needs to be spliced below 122 | } 123 | 124 | if(remainder1.isEmpty) { 125 | if(remainder2.isEmpty) // remove full overlap 126 | this.ranges.splice(beginPos,endPos-beginPos); 127 | else {// part of r1 remains 128 | this.ranges.splice(beginPos,endPos-beginPos,remainder2); 129 | } 130 | } else if(remainder2.isEmpty) // part of r0a remains 131 | this.ranges.splice(beginPos,endPos-beginPos,remainder1); 132 | else 133 | this.ranges.splice(beginPos,endPos-beginPos,remainder1,remainder2); 134 | } 135 | 136 | 137 | public applyEdit(delta: textUtil.RangeDelta) { 138 | for(let idx = 0; idx < this.ranges.length; ++idx) 139 | this.ranges[idx] = textUtil.rangeTranslate(this.ranges[idx], delta); 140 | } 141 | 142 | public toString() { 143 | return this.ranges.toString(); 144 | } 145 | 146 | 147 | /** 148 | * @returns the index of the range at or before the position, or -1 if no such range exists 149 | * case: [A] X [B] --> A 150 | * case: [A-X] [B] --> A 151 | */ 152 | private indexAt(position: vscode.Position, begin?: number, end?: number) : number { 153 | // binary search! 154 | if(!begin) 155 | begin = 0; 156 | if(!end) 157 | end = this.ranges.length; 158 | while (begin < end) { 159 | const pivot = (begin + end) >> 1; 160 | const pivotRange = this.ranges[pivot]; 161 | if(position.isBefore(pivotRange.start)) 162 | end = pivot; 163 | else if(position.isBefore(pivotRange.end)) 164 | return pivot; 165 | else if (begin == pivot) 166 | break; 167 | else 168 | begin = pivot; 169 | } 170 | return begin; 171 | } 172 | // 173 | // public insertShift(position: vscode.Position, linesDelta: number, charactersDelta: number) : boolean { 174 | // if(linesDelta == 0 && charactersDelta == 0) 175 | // return; 176 | // if(linesDelta < 0 || charactersDelta < 0) 177 | // return; 178 | // const beginIdx = this.indexAt(position); 179 | // const beginSent = this.ranges[beginIdx]; 180 | // if(beginSent.end.isAfter(position) { 181 | // // contains the position 182 | // 183 | // beginSent.end.translate(linesDelta).with(undefined,charactersDelta); 184 | // } else if(beginIdx < this.sentencesByPosition.length-1 185 | // && -count > this.sentencesByPosition[beginIdx+1].textBegin-beginSent.textEnd) { 186 | // return false; // cannot remove more characters than exist between sentences 187 | // } 188 | // 189 | // // shift subsequent sentences 190 | // for (let idx = beginIdx+1; idx < this.sentencesByPosition.length; ++idx) { 191 | // this.sentencesByPosition[idx].textBegin+= count; 192 | // this.sentencesByPosition[idx].textEnd+= count; 193 | // } 194 | // 195 | // return true; 196 | // } 197 | 198 | public getRanges() : vscode.Range[] { 199 | return this.ranges; 200 | } 201 | } 202 | // 203 | // // Adds or removes from the range, starting at position and affecting all subsequent ranges 204 | // public shift(position: vscode.Position, linesDelta: number, charactersDelta: number) : boolean { 205 | // if(linesDelta == 0 && charactersDelta == 0) 206 | // return; 207 | // const beginIdx = this.indexAt(position); 208 | // const beginSent = this.ranges[beginIdx]; 209 | // if(beginSent.end.isAfter(position) { 210 | // // contains the position 211 | // beginSent.end = tryTranslatePosition(beginSent.end, linesDelta, charactersDelta); 212 | // if(-count > beginSent.textEnd - beginSent.textBegin) 213 | // return false; // cannot remove more characters than a sentence has 214 | // beginSent.textEnd += count; 215 | // } else if(beginIdx < this.sentencesByPosition.length-1 216 | // && -count > this.sentencesByPosition[beginIdx+1].textBegin-beginSent.textEnd) { 217 | // return false; // cannot remove more characters than exist between sentences 218 | // } 219 | // 220 | // // shift subsequent sentences 221 | // for (let idx = beginIdx+1; idx < this.sentencesByPosition.length; ++idx) { 222 | // this.sentencesByPosition[idx].textBegin+= count; 223 | // this.sentencesByPosition[idx].textEnd+= count; 224 | // } 225 | // 226 | // return true; 227 | // } 228 | 229 | 230 | /* 231 | ) Starting ranges 232 | [*****|***] [**** 233 | *****] 234 | 1) insert on same line 235 | [*****<++++++>***] [**** 236 | *****] 237 | 2) insert with a line break 238 | [*****<+++ 239 | +++>***] [**** 240 | *****] 241 | 242 | ) Deleting on same line 243 | [***<----->**] [**** 244 | *****] 245 | = 246 | [*****] [**** 247 | *****] 248 | 249 | ) Deleting on multiple lines 250 | [***<-- 251 | ----->**] [**** 252 | *****] 253 | = 254 | [*****] [**** 255 | *****] 256 | 257 | */ 258 | // 259 | // function shiftPosition(pos: vscode.Position, linesDelta: number, charactersDelta: number) : vscode.Position { 260 | // if(linesDelta > 1) { 261 | // return new vscode.Position(linesDelta,pos.character - ); 262 | // } 263 | // if(linesDelta == 0 && ) { 264 | // 265 | // } 266 | // return null; 267 | // } -------------------------------------------------------------------------------- /src/document.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as copyPaste from 'copy-paste'; 3 | 4 | import {LanguageEntry, HideTextMethod} from './configuration'; 5 | import * as pos from './position'; 6 | //import {RangeSet} from './RangeSet'; 7 | //import {DisjointRangeSet} from './DisjointRangeSet'; 8 | //import * as drangeset from './DisjointRangeSet'; 9 | //import * as textUtil from './text-util'; 10 | import * as tm from './text-mate'; 11 | //import {MatchResult, iterateMatches, iterateMatchArray, mapIterator} from './regexp-iteration'; 12 | //import * as decorations from './decorations'; 13 | import {PrettyModel, UpdateDecorationEntry, UpdateDecorationInstanceEntry} from './PrettyModel'; 14 | 15 | //const debugging = false; 16 | const activeEditorDecorationTimeout = 100; 17 | const updateSelectionTimeout = 20; 18 | const inactiveEditorDecorationTimeout = 500; 19 | 20 | function arrayEqual(a1: T[], a2: T[], isEqual: (x:T,y:T)=>boolean = ((x,y) => x===y)) : boolean { 21 | if(a1.length!=a2.length) 22 | return false; 23 | for(let idx = 0; idx < a1.length; ++idx) { 24 | if(!isEqual(a1[idx],a2[idx])) 25 | return false; 26 | } 27 | return true; 28 | } 29 | 30 | class DebounceFunction implements vscode.Disposable { 31 | private timer?: NodeJS.Timer = null; 32 | private callback?: () => void = null; 33 | constructor(private timeout: number) {} 34 | public dispose() { 35 | if(this.timer != null) { 36 | clearTimeout(this.timer); 37 | this.timer = null; 38 | } 39 | } 40 | public call(callback: () => void): void { 41 | this.callback = callback; 42 | if (this.timer == null) { 43 | this.timer = setTimeout(() => { 44 | this.callback(); 45 | this.callback = null; 46 | this.timer = null; 47 | }, this.timeout); 48 | } 49 | } 50 | } 51 | 52 | export class PrettyDocumentController implements vscode.Disposable { 53 | private readonly model : PrettyModel; 54 | private readonly subscriptions : vscode.Disposable[] = []; 55 | private currentDecorations : UpdateDecorationEntry[] = []; 56 | private updateActiveEditor = new DebounceFunction(activeEditorDecorationTimeout); 57 | private updateInactiveEditors = new DebounceFunction(inactiveEditorDecorationTimeout); 58 | private updateSelection = new DebounceFunction(updateSelectionTimeout); 59 | 60 | public constructor(doc: vscode.TextDocument, settings: LanguageEntry, options: {hideTextMethod: HideTextMethod, textMateGrammar?: tm.IGrammar|null}, 61 | private document = doc, 62 | private adjustCursorMovement = settings.adjustCursorMovement, 63 | ) { 64 | const docModel = { 65 | getText: (r?:vscode.Range) => this.document.getText(r), 66 | getLine: (n:number) => this.document.lineAt(n).text, 67 | getLineRange: (n:number) => this.document.lineAt(n).range, 68 | getLineCount: () => this.document.lineCount, 69 | validateRange: (r: vscode.Range) => this.document.validateRange(r), 70 | } 71 | this.model = new PrettyModel(docModel,settings,options); 72 | 73 | this.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e) => { 74 | if(e.document == this.document) 75 | this.onChangeDocument(e); 76 | })); 77 | 78 | this.applyDecorations(this.getEditors(), this.model.getDecorationsList()); 79 | } 80 | 81 | public dispose() { 82 | this.model.dispose(); 83 | this.subscriptions.forEach((s) => s.dispose()); 84 | } 85 | 86 | private getEditors() { 87 | return vscode.window.visibleTextEditors 88 | .filter((editor) => { 89 | return editor.document.uri === this.document.uri; 90 | }); 91 | } 92 | 93 | public gotFocus() { 94 | this.applyDecorations(this.getEditors(), this.currentDecorations); 95 | } 96 | 97 | public copyDecorated(editor: vscode.TextEditor) : Promise { 98 | function doCopy(x: string) { 99 | return new Promise((resolve, reject) => copyPaste.copy(x, (err) => err ? reject(err) : resolve())); 100 | } 101 | const copy = editor.selections.map(sel => this.model.getDecoratedText(sel)); 102 | if(copy.length === 0) 103 | return Promise.resolve(); 104 | else 105 | return doCopy(copy.join('\n')) 106 | } 107 | 108 | private applyActiveEditorDecorations( 109 | editors: Iterable, 110 | decs: UpdateDecorationEntry[], 111 | revealRanges?: vscode.Range[], 112 | prettyCursors?: UpdateDecorationInstanceEntry, 113 | ): void { 114 | this.updateActiveEditor.call(() => { 115 | try { 116 | for(const editor of editors) { 117 | const cursors = prettyCursors 118 | || this.model.renderPrettyCursor(editor.selections); 119 | // Which ranges should *not* be prettified? 120 | const reveal = revealRanges 121 | || this.model.revealSelections(editor.selections).ranges 122 | decs.forEach(d => editor.setDecorations( 123 | d.decoration, 124 | // d.ranges.map(r => {range: r}) 125 | d.ranges 126 | // Decorate only those not revealed 127 | .filter(r => reveal.every(s => s.intersection(r) === undefined)) 128 | // Show cursors 129 | .map(r => ({ 130 | range: r, 131 | renderOptions: cursors && cursors.ranges.isOverlapping(r) 132 | ? cursors.decoration 133 | : undefined 134 | })) 135 | )); 136 | } 137 | } catch(err) { 138 | console.error(err) 139 | } 140 | }); 141 | } 142 | 143 | private applyInactiveEditorDecorations( 144 | editors: Iterable, 145 | decs: UpdateDecorationEntry[], 146 | ): void { 147 | this.updateInactiveEditors.call(() => { 148 | try { 149 | for(const editor of editors) { 150 | if(editor === vscode.window.activeTextEditor) 151 | continue; 152 | decs.forEach(d => editor.setDecorations(d.decoration, d.ranges)); 153 | } 154 | } catch(err) { 155 | console.error(err) 156 | } 157 | }); 158 | } 159 | 160 | private applyDecorations(editors: Iterable, decs: UpdateDecorationEntry[]) { 161 | this.currentDecorations = decs; 162 | this.applyActiveEditorDecorations([vscode.window.activeTextEditor], decs); 163 | this.applyInactiveEditorDecorations(editors, decs); 164 | } 165 | 166 | private onChangeDocument(event: vscode.TextDocumentChangeEvent) { 167 | if(this.model.applyChanges(event.contentChanges)) 168 | this.applyDecorations(this.getEditors(), this.model.getDecorationsList()) 169 | } 170 | 171 | public refresh() { 172 | this.model.recomputeDecorations(); 173 | this.applyDecorations(this.getEditors(), this.model.getDecorationsList()); 174 | } 175 | 176 | private lastSelections = new Map(); 177 | public adjustCursor(editor: vscode.TextEditor): null|vscode.Selection[] { 178 | let updated = false; 179 | const adjustedSelections : vscode.Selection[] = []; 180 | const before = this.lastSelections.get(editor); 181 | if(!before) { 182 | this.lastSelections.set(editor,editor.selections); 183 | return editor.selections; 184 | } 185 | const after = editor.selections; 186 | if(arrayEqual(before,after)) 187 | return null; 188 | 189 | after.forEach((sel,idx) => { 190 | if(before[idx] === undefined) { 191 | adjustedSelections.push(new vscode.Selection(sel.anchor,sel.active)); 192 | return; 193 | } 194 | const adjusted = pos.adjustCursorMovement(before[idx].active,sel.active,this.document,this.model.getPrettySubstitutionsRanges()); 195 | if(!adjusted.pos.isEqual(sel.active)) { 196 | updated = true; 197 | } 198 | 199 | // if anchor==active, then adjust both; otherwise just adjust the active position 200 | if(sel.anchor.isEqual(sel.active)) 201 | adjustedSelections.push(new vscode.Selection(adjusted.pos,adjusted.pos)); 202 | else 203 | adjustedSelections.push(new vscode.Selection(sel.anchor,adjusted.pos)); 204 | }); 205 | 206 | this.lastSelections.set(editor,adjustedSelections); 207 | 208 | // could cause this method to be called again, but since we've set the 209 | // last-selection to adjustedSelections, we will immediately return. 210 | if(updated) 211 | editor.selections = adjustedSelections; 212 | 213 | return adjustedSelections; 214 | } 215 | 216 | // Cache of revealed ranges (to prevent unnecessary updates) 217 | private revealedRanges: vscode.Range[] = []; 218 | private cursorRanges: vscode.Range[] = []; 219 | /** 220 | * The cursor has moved / the selection has changed. Reveal the original text, 221 | * box symbols, tec. as needed. 222 | * @param editor 223 | */ 224 | public selectionChanged(editor: vscode.TextEditor) { 225 | this.updateSelection.call(() => { 226 | let selections: null|vscode.Selection[]; 227 | if(this.adjustCursorMovement) { 228 | selections = this.adjustCursor(editor); 229 | } else { 230 | selections = editor.selections; 231 | } 232 | if(selections == null) { 233 | return; 234 | } 235 | 236 | const cursors = this.model.renderPrettyCursor(selections); 237 | const cR = cursors == null ? [] : cursors.ranges.getRanges(); 238 | const revealed = this.model.revealSelections(selections); 239 | if (!arrayEqual(revealed.ranges, this.revealedRanges) 240 | || !arrayEqual(cR, this.cursorRanges)) { 241 | this.applyActiveEditorDecorations( 242 | [editor], 243 | this.model.getDecorationsList(), 244 | revealed.ranges, 245 | cursors, 246 | ); 247 | } 248 | this.revealedRanges = revealed.ranges; 249 | this.cursorRanges = cR; 250 | }) 251 | 252 | // const r1 = this.model.revealSelections(editor.selections); 253 | // const r2 = this.model.renderPrettyCursor(editor.selections); 254 | // if(this.adjustCursorMovement) 255 | // this.adjustCursor(editor); 256 | 257 | // if(r1) { 258 | // editor.setDecorations(r1.decoration, r1.ranges); 259 | // } 260 | // if(r2) 261 | // editor.setDecorations(r2.decoration, r2.ranges); 262 | } 263 | 264 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI][action-shield]][action-link] 2 | [![Contributing][contributing-shield]][contributing-link] 3 | [![Code of Conduct][conduct-shield]][conduct-link] 4 | [![Zulip][zulip-shield]][zulip-link] 5 | 6 | [action-shield]: https://github.com/coq-community/vsc-conceal/actions/workflows/ci.yml/badge.svg?branch=main 7 | [action-link]: https://github.com/coq-community/vsc-conceal/actions?query=workflow:ci 8 | 9 | [contributing-shield]: https://img.shields.io/badge/contributions-welcome-%23f7931e.svg 10 | [contributing-link]: https://github.com/coq-community/manifesto/blob/master/CONTRIBUTING.md 11 | 12 | [conduct-shield]: https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-%23f15a24.svg 13 | [conduct-link]: https://github.com/coq-community/manifesto/blob/master/CODE_OF_CONDUCT.md 14 | 15 | [zulip-shield]: https://img.shields.io/badge/chat-on%20zulip-%23c1272d.svg 16 | [zulip-link]: https://coq.zulipchat.com/#narrow/stream/237662-VsCoq-devs.20.26.20users 17 | 18 | # Conceal for VSCode 19 | 20 | Conceal makes *visual* substitutions to your source code, e.g. displaying `fun` as `λ`, while never touching your code. 21 | 22 | This feature is inspired by [prettify-symbols-mode for Emacs](https://www.emacswiki.org/emacs/PrettySymbol) and is the unofficial successor of [vsc-prettify-symbols-mode](https://github.com/siegebell/vsc-prettify-symbols-mode). 23 | 24 | This extension is currently developed and maintained as part of 25 | [Romain Tetley](https://github.com/rtetley), 26 | and contributors. 27 | 28 | ## Configuration 29 | 30 | Once you have installed this extension, modify `settings.json` to add language-specific substitutions. For example, the following settings will target F# files, rendering `fun` as `λ`, `->` as `⟶`, and place a border around parameters. 31 | ```json 32 | "conceal.substitutions": [{ 33 | "language": "fsharp", 34 | "substitutions": [ 35 | { "ugly": "fun", "pretty": "λ", "scope": "keyword.other.function-definition.fsharp" }, 36 | { "ugly": "->", "pre": "[^->]", "post": "[^->]", "pretty": "⟶" }, 37 | { "ugly": ".+", "scope": "variable.parameter.fsharp", "pre": "^", "post": "$", "style": { "border": "1pt solid green" } } 38 | ] 39 | }] 40 | ``` 41 | 42 | A substitution matches any string that satisfies the `"ugly"` pattern, visually replacing it with `"pretty"` and/or applying style via `"style"`. You can optionally specify the context by providing `"pre"` or `"post"` regular expressions that must be matched for the substitution to occur. Or you can specify a syntactic scope in which to perform the substitution. You can also target multiple languages or glob patterns at once via `"languages": ["fsharp", {"pattern": "**/*.txt"}]`. 43 | 44 | ### Scopes 45 | 46 | *Note: scope support is experimental and only available on versions of vscode older than 1.21.1*. 47 | 48 | By default, regular expressions match against a whole line of text. If `"scope"` is specified, then regular expression matches will only be performed on the parsed [TextMate] tokens that match the given scope. A small subset of TextMate scope expressions are supported. For example, a substitution with scope `"source.js comment"` will match a token with scope `"text.html.basic source.js comment.block.html"`. A scoped `"ugly"` regular expression must match the entire token by default -- i.e. `"pre"` and `"post"` are respectively set to `"^"` and `"$"` by default when a scope is specified. However, `"pre"` and `"post"` can be overriden to allow multiple substitutions within a single token (e.g. a comment). 49 | 50 | *Tip: use [scope-info](https://marketplace.visualstudio.com/items?itemName=siegebell.scope-info) to see the scope assigned to each token in your source.* 51 | 52 | ### Revealing symbols 53 | 54 | By default, "ugly" text will be revealed while contacted by a cursor. You may override this behavior by specifying `"conceal.revealOn"`, or per-language by specifying `"revealOn"` within a language entry. Options are: 55 | * `"cursor"`: reveal while a cursor contacts the symbol (default); 56 | * `"cursor-inside"`: reveal while a cursor is *inside* the symbol; 57 | * `"active-line"`: reveal all symbols while on the same line as a cursor; 58 | * `"selection"`: reveal all symbols while being selected or in contact with a cursor; or 59 | * `"none"`: do not reveal symbols. 60 | 61 | ### Pretty cursor 62 | 63 | By default, any "pretty" symbol that comes into contact with the cursor will be rendered with a box outline around it. This effect is only visible if the "ugly" text is not revealed (e.g. `"revealOn": "none"`). You can control this setting by specifying `"conceal.prettyCursor"`, or per-language by specifying `"prettyCursor"` within a language entry. Options are: 64 | * `"boxed"`: display a box around a symbol (only visible if the "ugly" text is not revealed); or 65 | * `"none"`: do not change the appearance of the symbol. 66 | 67 | ### Adjust cursor movement 68 | 69 | By default, cursor movement will traverse the characters of the "ugly" text -- this will cause it to become invisible while inside the text if it is not revealed (see `"revealOn"`). Setting `"conceal.adjustCursorMovement"` to `true` will tweak cursor movement so that "pretty" symbols behave as a single character. This can be overriden per-language by specifying `"adjustCursorMovement"` in a language entry. In particular, left or right movement will cause the cursor to jump over the symbol instead of going inside. However, this setting does not currently account for all kinds of cursor movement, e.g. up/down. 70 | 71 | ### Styling 72 | 73 | A tiny subset of CSS can be used to apply styling to the substitution text by setting `"style"`; styles can be specialized for light and dark themes. If `"pretty"` is not specified, then`"style"` must be specified: the result being that all "ugly" matches will have the style applied to them instead of being substituted. 74 | 75 | * Supported styles: `"border", "backgroundColor", "color", "textDecoration"` (this list is limited by vscode). 76 | * Themed: e.g. `"dark": {"color": "white"}, "light": {"color": "black"}` 77 | * Unsupported styles: e.g. `"hackCSS": "font-style: italic, font-size: 2em"` (this can easily break rendering) 78 | 79 | ### Regular expressions 80 | 81 | This extension uses [Javascript's regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) syntax for `"ugly"`, `"pre"`, and `"post"` (but double-escaped because it is parsed by both JSON and regexp). You must avoid using capturing-groups or mis-parenthesized expressions as it will cause substitutions to behave unpredictably (validation is not performed so you will not receive an error message). 82 | 83 | ### Commands 84 | 85 | The following commands are available for keybinding: 86 | * `conceal.copyWithSubstitutions`: copy selected text with "pretty" substitutions applied 87 | * `conceal.enablePrettySymbols`: globally *enable* prettify symbols mode 88 | * `conceal.disablePrettySymbols`: globally *disable* prettify symbols mode 89 | * `conceal.togglePrettySymbols`: globally *toggle* prettify symbols mode 90 | 91 | 92 | ### Common settings for `settings.json` 93 | 94 | * **Default:** symbols are unfolded as they are traversed by the cursor. 95 | ```json 96 | "conceal.renderOn": "cursor", 97 | "conceal.adjustCursorMovement": false, 98 | ``` 99 | * Suggested alternative: symbols are never unfolded and generally act like a single character w.r.t. cursor movement. 100 | ```json 101 | "conceal.renderOn": "none", 102 | "conceal.adjustCursorMovement": true, 103 | ``` 104 | 105 | ## Variable-width symbols driving you crazy? 106 | 107 | Check out [*Monospacifier*](https://github.com/cpitclaudel/monospacifier) to fix your fonts! 108 | 109 | ![example fix for variable-width fonts](https://github.com/cpitclaudel/monospacifier/blob/master/demo/symbola-loop.gif?raw=true) 110 | 111 | **Quick start example:** if your editor font is Consolas, download and install the [XITS Math fallback font for Consolas](https://github.com/cpitclaudel/monospacifier/blob/master/fonts/XITSMath_monospacified_for_Consolas.ttf?raw=true), then add the following to `settings.json`: 112 | ```json 113 | "editor.fontFamily": "Consolas, 'XITS Math monospacified for Consolas', 'Courier New', monospace" 114 | ``` 115 | 116 | ## Known issues: 117 | 118 | *Tip: [submit new issues on github](https://github.com/BRBoer/vsc-conceal/issues)* 119 | * You can write bad regular expressions that break substitutions and you will not get an error message. 120 | * The substitutions sometimes get into an inconsistent state when editing. To resolve, reenable prettify-symbols-mode -- this will cause the whole document to be reparsed. 121 | * The Live Snippets feature from the LaTeX Utilities extension will not function when `"conceal.adjustCursorMovement"` is set to `true` 122 | 123 | ## Examples 124 | [See the wiki for more examples ‐ and contribute your own!](https://github.com/siegebell/vsc-prettify-symbols-mode/wiki) 125 | 126 | The following shows a brief subset of useful substitutions for Haskell, OCaml, and F#: 127 | ```json 128 | "conceal.revealOn": "cursor", 129 | "conceal.adjustCursorMovement": false, 130 | "conceal.substitutions": [{ 131 | "language": "haskell", 132 | "revealOn": "active-line", 133 | "substitutions": [ 134 | { "ugly": "\\\\", "pretty": "λ", "post": "\\s*(?:\\w|_).*?\\s*->" }, 135 | { "ugly": "->", "pretty": "→" }, 136 | { "ugly": "==", "pretty": "≡" }, 137 | { "ugly": "not\\s?", "pretty": "¬", "pre": "\\b", "post": "\\b" }, 138 | { "ugly": ">", "pretty": ">", "pre": "[^=\\-<>]|^", "post": "[^=\\-<>]|$" }, 139 | { "ugly": "<", "pretty": "<", "pre": "[^=\\-<>]|^", "post": "[^=\\-<>]|$" }, 140 | { "ugly": ">=", "pretty": "≥", "pre": "[^=\\-<>]|^", "post": "[^=\\-<>]|$" }, 141 | { "ugly": "<=", "pretty": "≤", "pre": "[^=\\-<>]|^", "post": "[^=\\-<>]|$" } 142 | ]},{ 143 | "language": ["ocaml", {"pattern": "**/*.{ml}"}], 144 | "revealOn": "none", 145 | "adjustCursorMovement": true, 146 | "substitutions": [ 147 | { "ugly": "fun", "pretty": "λ", "pre": "\\b", "post": "\\b" }, 148 | { "ugly": "->", "pretty": "→", "pre": "[^->]", "post": "[^->]" }, 149 | { "ugly": "List[.]for_all", "pretty": "∀", "pre": "\\b", "post": "\\b" }, 150 | { "ugly": "List[.]exists", "pretty": "∃", "pre": "\\b", "post": "\\b" }, 151 | { "ugly": "List[.]mem", "pretty": "∈", "pre": "\\b", "post": "\\b" }, 152 | { "ugly": "\\|", "pretty": "║", "pre": "^\\s+" } 153 | ]},{ 154 | "language": "fsharp", 155 | "substitutions": [ 156 | { "ugly": "fun", "pretty": "λ", "pre": "\\b", "post": "\\b" }, 157 | { "ugly": "->", "pretty": "→", "pre": "[^->]", "post": "[^->]" }, 158 | { "ugly": "List[.]forall", "pretty": "∀", "pre": "\\b", "post": "\\b" }, 159 | { "ugly": "List[.]exists", "pretty": "∃", "pre": "\\b", "post": "\\b" }, 160 | { "ugly": ">>", "pretty": "≫", "pre": "[^=<>]|^", "post": "[^=<>]|$" }, 161 | { "ugly": "<<", "pretty": "≪", "pre": "[^=<>]|^", "post": "[^=<>]|$" }, 162 | { "ugly": "\\|", "pretty": "║", "pre": "^\\s+" } 163 | ]}] 164 | ``` 165 | -------------------------------------------------------------------------------- /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 { Settings, LanguageEntry, UglyRevelation, PrettyCursor, HideTextMethod } from './configuration'; 8 | import { PrettyDocumentController } from './document'; 9 | import * as api from './api'; 10 | import * as tm from './text-mate'; 11 | 12 | /** globally enable or disable all substitutions */ 13 | let prettySymbolsEnabled = true; 14 | 15 | /** Tracks all documents that substitutions are being applied to */ 16 | const documents = new Map(); 17 | /** The current configuration */ 18 | let settings: Settings; 19 | 20 | const onEnabledChangeHandlers = new Set<(enabled: boolean) => void>(); 21 | export const additionalSubstitutions = new Set(); 22 | export let textMateRegistry: tm.Registry; 23 | 24 | interface ExtensionGrammar { 25 | language?: string, scopeName?: string, path?: string, embeddedLanguages?: { [scopeName: string]: string }, injectTo?: string[] 26 | } 27 | interface ExtensionPackage { 28 | contributes?: { 29 | languages?: { id: string, configuration: string }[], 30 | grammars?: ExtensionGrammar[], 31 | } 32 | } 33 | 34 | function getLanguageScopeName(languageId: string): string { 35 | try { 36 | const languages = 37 | vscode.extensions.all 38 | .filter(x => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars) 39 | .reduce((a: ExtensionGrammar[], b) => [...a, ...(b.packageJSON as ExtensionPackage).contributes.grammars], []); 40 | const matchingLanguages = languages.filter(g => g.language === languageId); 41 | 42 | if (matchingLanguages.length > 0) { 43 | console.info(`Mapping language ${languageId} to initial scope ${matchingLanguages[0].scopeName}`); 44 | return matchingLanguages[0].scopeName; 45 | } 46 | } catch (err) { 47 | console.log(err); 48 | } 49 | console.info(`Cannot find a mapping for language ${languageId}; assigning default scope source.${languageId}`); 50 | return 'source.' + languageId; 51 | } 52 | 53 | const grammarLocator: tm.IGrammarLocator = { 54 | getFilePath: function (scopeName: string): string { 55 | try { 56 | const grammars = 57 | vscode.extensions.all 58 | .filter(x => x.packageJSON && x.packageJSON.contributes && x.packageJSON.contributes.grammars) 59 | .reduce((a: (ExtensionGrammar & { extensionPath: string })[], b) => [...a, ...(b.packageJSON as ExtensionPackage).contributes.grammars.map(x => Object.assign({ extensionPath: b.extensionPath }, x))], []); 60 | const matchingLanguages = grammars.filter(g => g.scopeName === scopeName); 61 | 62 | if (matchingLanguages.length > 0) { 63 | const ext = matchingLanguages[0]; 64 | const file = path.join(ext.extensionPath, ext.path); 65 | console.info(`Found grammar for ${scopeName} at ${file}`) 66 | return file; 67 | } 68 | } catch (err) { 69 | console.log(err); 70 | } 71 | return undefined; 72 | } 73 | } 74 | 75 | /** initialize everything; main entry point */ 76 | export function activate(context: vscode.ExtensionContext): api.ConcealSymbolsMode { 77 | function registerTextEditorCommand(commandId: string, run: (editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args: string[]) => void): void { 78 | context.subscriptions.push(vscode.commands.registerTextEditorCommand(commandId, run)); 79 | } 80 | function registerCommand(commandId: string, run: (...args: string[]) => void): void { 81 | context.subscriptions.push(vscode.commands.registerCommand(commandId, run)); 82 | } 83 | 84 | registerTextEditorCommand('conceal.copyWithSubstitutions', copyWithSubstitutions); 85 | registerCommand('conceal.disablePrettySymbols', disablePrettySymbols); 86 | registerCommand('conceal.enablePrettySymbols', enablePrettySymbols); 87 | registerCommand('conceal.togglePrettySymbols', () => { 88 | if (prettySymbolsEnabled) { 89 | disablePrettySymbols(); 90 | } else { 91 | enablePrettySymbols(); 92 | } 93 | }); 94 | 95 | registerCommand('extension.disablePrettySymbols', () => { vscode.window.showErrorMessage('Command "extension.disablePrettySymbols" is deprecated; use "conceal.disablePrettySymbols" instead.') }); 96 | registerCommand('extension.enablePrettySymbols', () => { vscode.window.showErrorMessage('Command "extension.enablePrettySymbols" is deprecated; use "conceal.enablePrettySymbols" instead.') }); 97 | registerCommand('extension.togglePrettySymbols', () => { vscode.window.showErrorMessage('Command "extension.togglePrettySymbols" is deprecated; use "conceal.togglePrettySymbols" instead.') }); 98 | 99 | context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(selectionChanged)); 100 | 101 | context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(openDocument)); 102 | context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(closeDocument)); 103 | context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(onConfigurationChanged)); 104 | 105 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(changeActiveTextEditor)); 106 | 107 | reloadConfiguration(); 108 | 109 | const result: api.ConcealSymbolsMode = { 110 | onDidEnabledChange: function (handler: (enabled: boolean) => void): vscode.Disposable { 111 | onEnabledChangeHandlers.add(handler); 112 | return { 113 | dispose() { 114 | onEnabledChangeHandlers.delete(handler); 115 | } 116 | } 117 | }, 118 | isEnabled: function (): boolean { 119 | return prettySymbolsEnabled; 120 | }, 121 | registerSubstitutions: function (substitutions: api.LanguageEntry): vscode.Disposable { 122 | additionalSubstitutions.add(substitutions); 123 | // TODO: this could be smart about not unloading & reloading everything 124 | reloadConfiguration(); 125 | return { 126 | dispose() { 127 | additionalSubstitutions.delete(substitutions); 128 | } 129 | } 130 | } 131 | }; 132 | 133 | return result; 134 | } 135 | 136 | function copyWithSubstitutions(editor: vscode.TextEditor) { 137 | try { 138 | if (!editor) 139 | return; 140 | const prettyDoc = documents.get(editor.document.uri); 141 | if (prettyDoc) 142 | prettyDoc.copyDecorated(editor); 143 | } catch (e) { 144 | console.log(e); 145 | } 146 | } 147 | 148 | function changeActiveTextEditor(editor: vscode.TextEditor) { 149 | try { 150 | if (!editor) 151 | return; 152 | const prettyDoc = documents.get(editor.document.uri); 153 | if (prettyDoc) 154 | prettyDoc.gotFocus(); 155 | } catch (e) { 156 | console.log(e); 157 | } 158 | } 159 | 160 | 161 | /** A text editor selection changed; forward the event to the relevant document */ 162 | function selectionChanged(event: vscode.TextEditorSelectionChangeEvent) { 163 | try { 164 | const prettyDoc = documents.get(event.textEditor.document.uri); 165 | if (prettyDoc) 166 | prettyDoc.selectionChanged(event.textEditor); 167 | } catch (e) { 168 | console.error(e); 169 | } 170 | } 171 | 172 | /** Te user updated their settings.json */ 173 | function onConfigurationChanged() { 174 | reloadConfiguration(); 175 | } 176 | 177 | /** Re-read the settings and recreate substitutions for all documents */ 178 | function reloadConfiguration() { 179 | try { 180 | textMateRegistry = new tm.Registry(grammarLocator); 181 | } catch (err) { 182 | textMateRegistry = undefined; 183 | console.error(err); 184 | } 185 | 186 | const configuration = vscode.workspace.getConfiguration("conceal"); 187 | settings = { 188 | substitutions: configuration.get("substitutions", []), 189 | revealOn: configuration.get("revealOn", "cursor"), 190 | adjustCursorMovement: configuration.get("adjustCursorMovement", false), 191 | prettyCursor: configuration.get("prettyCursor", "boxed"), 192 | hideTextMethod: configuration.get("hideTextMethod", "hack-letterSpacing"), 193 | }; 194 | 195 | // Set default values for language-properties that were not specified 196 | for (const language of settings.substitutions) { 197 | if (language.revealOn === undefined) 198 | language.revealOn = settings.revealOn; 199 | if (language.adjustCursorMovement === undefined) 200 | language.adjustCursorMovement = settings.adjustCursorMovement; 201 | if (language.prettyCursor === undefined) 202 | language.prettyCursor = settings.prettyCursor; 203 | if (language.combineIdenticalScopes === undefined) 204 | language.combineIdenticalScopes = false; 205 | } 206 | 207 | // Recreate the documents 208 | unloadDocuments(); 209 | for (const doc of vscode.workspace.textDocuments) 210 | openDocument(doc); 211 | } 212 | 213 | function disablePrettySymbols() { 214 | prettySymbolsEnabled = false; 215 | onEnabledChangeHandlers.forEach(h => h(false)); 216 | unloadDocuments(); 217 | } 218 | 219 | function enablePrettySymbols() { 220 | prettySymbolsEnabled = true; 221 | onEnabledChangeHandlers.forEach(h => h(true)); 222 | reloadConfiguration(); 223 | } 224 | 225 | 226 | /** Attempts to find the best-matching language entry for the language-id of the given document. 227 | * @param the document to match 228 | * @returns the best-matching language entry, or else `undefined` if none was found */ 229 | function getLanguageEntry(doc: vscode.TextDocument): LanguageEntry { 230 | const rankings = settings.substitutions 231 | .map((entry) => ({ rank: vscode.languages.match(entry.language, doc), entry: entry })) 232 | .filter(score => score.rank > 0) 233 | .sort((x, y) => (x.rank > y.rank) ? -1 : (x.rank == y.rank) ? 0 : 1); 234 | 235 | const entry: LanguageEntry = rankings.length > 0 236 | ? Object.assign({}, rankings[0].entry) 237 | : { 238 | language: doc.languageId, 239 | substitutions: [], 240 | adjustCursorMovement: settings.adjustCursorMovement, 241 | revealOn: settings.revealOn, 242 | prettyCursor: settings.prettyCursor, 243 | combineIdenticalScopes: true, 244 | }; 245 | 246 | for (const language of additionalSubstitutions) { 247 | if (vscode.languages.match(language.language, doc) > 0) { 248 | entry.substitutions.push(...language.substitutions); 249 | } 250 | } 251 | 252 | return entry; 253 | } 254 | 255 | async function loadGrammar(scopeName: string): Promise { 256 | return new Promise((resolve, reject) => { 257 | try { 258 | textMateRegistry.loadGrammar(scopeName, (err, grammar) => { 259 | if (err) 260 | reject(err) 261 | else 262 | resolve(grammar); 263 | }) 264 | } catch (err) { 265 | reject(err); 266 | } 267 | }) 268 | } 269 | 270 | async function openDocument(doc: vscode.TextDocument) { 271 | if (!prettySymbolsEnabled) 272 | return; 273 | try { 274 | const prettyDoc = documents.get(doc.uri); 275 | if (prettyDoc) { 276 | prettyDoc.refresh(); 277 | } else { 278 | const language = getLanguageEntry(doc); 279 | if (language && language.substitutions.length > 0) { 280 | const usesScopes = language.substitutions.some(s => s.scope !== undefined); 281 | let grammar: tm.IGrammar = undefined; 282 | if (textMateRegistry && usesScopes) { 283 | try { 284 | const scopeName = language.textMateInitialScope || getLanguageScopeName(doc.languageId); 285 | grammar = await loadGrammar(scopeName); 286 | } catch (error) { 287 | console.error(error); 288 | } 289 | } 290 | documents.set(doc.uri, new PrettyDocumentController(doc, language, { hideTextMethod: settings.hideTextMethod, textMateGrammar: grammar })); 291 | } 292 | } 293 | } catch (err) { 294 | console.log(err); 295 | } 296 | } 297 | 298 | function closeDocument(doc: vscode.TextDocument) { 299 | const prettyDoc = documents.get(doc.uri); 300 | if (prettyDoc) { 301 | prettyDoc.dispose(); 302 | documents.delete(doc.uri); 303 | } 304 | } 305 | 306 | function unloadDocuments() { 307 | for (const prettyDoc of documents.values()) { 308 | prettyDoc.dispose(); 309 | } 310 | documents.clear(); 311 | } 312 | 313 | /** clean-up; this extension is being unloaded */ 314 | export function deactivate() { 315 | onEnabledChangeHandlers.forEach(h => h(false)); 316 | unloadDocuments(); 317 | } 318 | 319 | -------------------------------------------------------------------------------- /src/DisjointRangeSet.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as textUtil from './text-util'; 3 | 4 | 5 | /// Tracks a set of disjoint (nonoverlapping) ranges 6 | /// Internallay sorts the ranges and provides fast lookup/intersection 7 | /// Inserting a range fails if it overlaps another range 8 | export class DisjointRangeSet { 9 | // ranges, sorted by starting position; nonoverlapping 10 | private ranges : vscode.Range[]; 11 | 12 | constructor() { 13 | this.ranges = []; 14 | } 15 | 16 | private static makeFromSortedRanges(ranges:vscode.Range[]) : DisjointRangeSet { 17 | const result = new DisjointRangeSet(); 18 | result.ranges = ranges; 19 | return result; 20 | } 21 | 22 | public getStart() : vscode.Position { 23 | if(this.ranges.length > 0) 24 | return this.ranges[0].start; 25 | else 26 | return new vscode.Position(0,0); 27 | } 28 | 29 | public getEnd() : vscode.Position { 30 | if(this.ranges.length > 0) 31 | return this.ranges[this.ranges.length-1].end; 32 | else 33 | return new vscode.Position(0,0); 34 | } 35 | 36 | public getTotalRange() { 37 | if(this.ranges.length > 0) 38 | return new vscode.Range(this.ranges[0].start,this.ranges[this.ranges.length-1].end); 39 | else 40 | return new vscode.Range(0,0,0,0); 41 | } 42 | 43 | public isEmpty() : boolean { 44 | return this.ranges.length == 0; 45 | } 46 | 47 | public isEmptyRange() : boolean { 48 | return this.ranges.length == 0 || this.ranges[0].start.isEqual(this.ranges[this.ranges.length-1].end); 49 | } 50 | 51 | /// assumes that the ranges in newRanges are not interspersed with this 52 | /// this assumption can be made if the overlapping range had been previously removed 53 | public insertRanges(newRanges: DisjointRangeSet) : boolean { 54 | const totalRange = newRanges.getTotalRange() 55 | const insertionIdx = this.findIndex(totalRange.start); 56 | const next = this.ranges[insertionIdx]; 57 | if(next && totalRange.end.isAfter(next.start)) 58 | return false; 59 | // not overlapping! 60 | this.ranges.splice(insertionIdx, 0, ...newRanges.ranges); 61 | } 62 | 63 | /** 64 | * @returns `true` on success: if there was no overlap 65 | */ 66 | public insert(range: vscode.Range) : boolean { 67 | const insertionIdx = this.findIndex(range.start); 68 | const next = this.ranges[insertionIdx]; 69 | if(next && range.end.isAfter(next.start)) 70 | return false; 71 | // not overlapping! 72 | this.ranges.splice(insertionIdx, 0, range); 73 | return true; 74 | } 75 | 76 | public isOverlapping(range: vscode.Range) : boolean { 77 | const insertionIdx = this.findIndex(range.start); 78 | const next = this.ranges[insertionIdx]; 79 | if(next && range.end.isAfter(next.start)) 80 | return true; 81 | return false; 82 | } 83 | 84 | // remove all ranges that overlap the given range 85 | public removeOverlapping(range: vscode.Range, options: {includeTouchingStart?:boolean,includeTouchingEnd?:boolean} = {includeTouchingStart:false,includeTouchingEnd:false}) : vscode.Range[] { 86 | const inclStart = options.includeTouchingStart || false; 87 | const inclEnd = options.includeTouchingEnd || false; 88 | let begin = this.findIndex(range.start); 89 | if(inclStart && begin > 0 && this.ranges[begin-1].end.isEqual(range.start)) 90 | --begin; 91 | let end = begin; 92 | while(end < this.ranges.length && this.ranges[end].start.isBefore(range.end)) 93 | ++end; 94 | if(inclEnd && end < this.ranges.length && this.ranges[end].start.isEqual(range.end)) 95 | ++end; 96 | // else if(inclEnd && end < this.ranges.length-1 && this.ranges[end+1].start.isEqual(range.end)) 97 | // ++end; 98 | return this.ranges.splice(begin, end-begin); 99 | } 100 | 101 | // remove the given ranges (only considers equal ranges; not simply overlapping) 102 | public subtractRanges(ranges: DisjointRangeSet) : DisjointRangeSet { 103 | let idx1 = 0; 104 | const newRanges = this.ranges.filter((r) => { 105 | const idx = ranges.ranges.indexOf(r, idx1); 106 | if(idx != -1) { 107 | idx1 = idx; // take advantage of the fact the ranges are sorted 108 | return false; // discard 109 | } 110 | else 111 | return true; // keep 112 | }); 113 | return DisjointRangeSet.makeFromSortedRanges(newRanges); 114 | } 115 | 116 | private validateFoundIndex(idx: number, pos: vscode.Position, exclStart: boolean, inclEnd: boolean) { 117 | if(idx > this.ranges.length || idx < 0) 118 | throw "idx is out of range: idx > this.ranges.length || idx < 0"; 119 | else if(this.ranges.length == 0) 120 | return; 121 | else if(idx < this.ranges.length && !textUtil.rangeContains(this.ranges[idx],pos,exclStart,inclEnd) && this.ranges[idx].start.isBefore(pos)) 122 | throw "idx is too big; range comes before pos"; 123 | else if(idx > 0 && this.ranges[idx-1].end.isAfter(pos)) 124 | throw "idx is too big; previous element comes after pos"; 125 | else if(idx < this.ranges.length-1 && this.ranges[idx+1].start.isBefore(pos) && (!inclEnd || !this.ranges[idx].end.isEqual(pos))) 126 | throw "idx is too small; next element comes before pos"; 127 | } 128 | 129 | 130 | /** returns the index of the range that starts at or after the given position 131 | * if pos is after all range-starts, then this returns this.ranges.length 132 | */ 133 | private findIndex(pos: vscode.Position, options: {excludeStart?: boolean, includeEnd?: boolean} = {excludeStart: false, includeEnd: false}) { 134 | const exclStart = options.excludeStart || false; 135 | const inclEnd = options.includeEnd || false; 136 | 137 | let begin = 0; 138 | let end = this.ranges.length; 139 | if(this.ranges.length == 0) 140 | return 0; 141 | if(this.ranges[this.ranges.length-1].end.isBeforeOrEqual(pos) && (!inclEnd || !this.ranges[this.ranges.length-1].end.isEqual(pos))) 142 | return this.ranges.length; 143 | 144 | // binary search 145 | let idx = Math.floor((begin + end)/2); 146 | while(begin < end) { 147 | const range = this.ranges[idx]; 148 | if(textUtil.rangeContains(range,pos,exclStart,inclEnd)) 149 | break; // we've found a match 150 | else if(pos.isBefore(range.start) || (exclStart && pos.isEqual(range.start))) 151 | end = idx; 152 | else // pos.isAfterOrEqual(range.end) 153 | begin = idx+1; 154 | idx = Math.floor((begin + end)/2); 155 | } 156 | 157 | this.validateFoundIndex(idx,pos,exclStart,inclEnd); 158 | return idx; 159 | 160 | // if(begin < end) { 161 | // // we found a match 162 | // // rewind until we find the first matching index 163 | // // while(idx > 0 && this.ranges[idx-1].start.isEqual(pos)) 164 | // // --idx; 165 | // return idx; 166 | // } else { 167 | // // we did not directly find the item; advance to the nearest matching index (after pos) 168 | // for(let idx = begin; idx < this.ranges.length; ++idx) { 169 | // const range = this.ranges[idx]; 170 | // if(range.start.isAfter(pos)) 171 | // return idx; 172 | // } 173 | // // the position is after all values in the array 174 | // return this.ranges.length; 175 | // } 176 | } 177 | 178 | 179 | // private indexAt(pos: vscode.Position, options: {excludeStart?: boolean, excludeEnd?: boolean} = {excludeStart: false, excludeEnd: false}) : number { 180 | // const exclStart = options.excludeStart || false; 181 | // const exclEnd = options.excludeEnd || false; 182 | // let begin = 0; 183 | // let end = this.ranges.length; 184 | 185 | // // binary search 186 | // while(begin < end) { 187 | // const idx = Math.floor((begin + end)/2); 188 | // const range = this.ranges[idx]; 189 | // if(range.contains(pos) && !(exclStart && range.start.isEqual(pos)) && !(exclEnd && range.end.isEqual(pos))) 190 | // return idx; 191 | // else if(pos.isBefore(range.start)) 192 | // end = idx; 193 | // else 194 | // begin = idx+1; 195 | // } 196 | 197 | // // we did not directly find the item 198 | // for(let idx = begin; idx < this.ranges.length; ++idx) { 199 | // const range = this.ranges[idx]; 200 | // if(range.start.isAfterOrEqual(pos) && !(exclStart && range.start.isEqual(pos))) 201 | // return idx; 202 | // } 203 | // return -1; 204 | // } 205 | 206 | /** returns the range that contains pos, or else undefined */ 207 | public find(pos: vscode.Position, options: {excludeStart?: boolean, includeEnd?: boolean} = {excludeStart: false, includeEnd: false}) : vscode.Range { 208 | const idx = this.findIndex(pos, options); 209 | const match = this.ranges[idx]; 210 | if(match && textUtil.rangeContains(match,pos,options.excludeStart,options.includeEnd)) 211 | return match; 212 | else 213 | return undefined; 214 | } 215 | 216 | public findPreceding(pos: vscode.Position) : vscode.Range { 217 | const idx = this.findIndex(pos); 218 | const match = this.ranges[idx-1]; 219 | return match; 220 | } 221 | 222 | public *getRangesStartingAt(pos: vscode.Position) : IterableIterator { 223 | for(let idx = this.findIndex(pos); idx < this.ranges.length; ++idx) 224 | yield this.ranges[idx]; 225 | } 226 | 227 | public getRanges() { 228 | return this.ranges; 229 | } 230 | 231 | // /** removes any overlapping ranges and shifts the positions of subsequent ranges 232 | // * returns the removed ranges 233 | // */ 234 | // public shiftDeleteRange(del: vscode.Range) : vscode.Range[] { 235 | // if(this.ranges.length == 0 || del.isEmpty) 236 | // return []; 237 | // const removed = this.removeOverlapping(del); 238 | 239 | // const deltaLines = del.end.line - del.start.line; 240 | // // -delta for everything on the same line as del.end 241 | // const startCharacterDelta = (del.start.line==del.end.line) 242 | // ? del.end.character-del.start.character 243 | // : del.end.character; 244 | 245 | // let idx = this.findIndex(del.end); 246 | // // shift everything on the same line 247 | // for( ; idx < this.ranges.length && del.end.line == this.ranges[idx].start.line; ++idx) { 248 | // const range = this.ranges[idx]; 249 | // if(range.end.line==range.start.line) 250 | // this.ranges[idx] = new vscode.Range(range.start.translate(0,-startCharacterDelta), range.end.translate(0,-startCharacterDelta)); 251 | // else // should effectively break after this case 252 | // this.ranges[idx] = new vscode.Range(range.start.translate(0,-startCharacterDelta), range.end.translate(-deltaLines,0)); 253 | // } 254 | 255 | // // shift the remaining lines 256 | // for( ; idx < this.ranges.length; ++idx) { 257 | // const range = this.ranges[idx]; 258 | // this.ranges[idx] = new vscode.Range(range.start.translate(-deltaLines,0), range.end.translate(-deltaLines,0)); 259 | // } 260 | 261 | // return removed; 262 | // } 263 | 264 | // /** inserts a range; assumes nothing overlaps with pos 265 | // * returns true if successful; false if not (there was an overlap) 266 | // */ 267 | // public shiftInsertRange(pos: vscode.Position, insert: vscode.Range) : boolean { 268 | // if(this.ranges.length == 0 || insert.isEmpty) 269 | // return; 270 | // if(this.find(pos)) 271 | // return false; // overlap! 272 | 273 | // const deltaLines = insert.end.line - insert.start.line; 274 | // // delta for everything on the same line as insert.end 275 | // const startCharacterDelta = (insert.start.line==insert.end.line) 276 | // ? insert.end.character-insert.start.character 277 | // : insert.end.character; 278 | 279 | // let idx = this.findIndex(pos); 280 | // // shift everything on the same line 281 | // for( ; idx < this.ranges.length && pos.line == this.ranges[idx].start.line; ++idx) { 282 | // const range = this.ranges[idx]; 283 | // if(range.end.line==range.start.line) 284 | // this.ranges[idx] = new vscode.Range(range.start.translate(0,startCharacterDelta), range.end.translate(0,startCharacterDelta)); 285 | // else // should effectively break after this case 286 | // this.ranges[idx] = new vscode.Range(range.start.translate(0,startCharacterDelta), range.end.translate(deltaLines,0)); 287 | // } 288 | 289 | // // shift the remaining lines 290 | // for( ; idx < this.ranges.length; ++idx) { 291 | // const range = this.ranges[idx]; 292 | // this.ranges[idx] = new vscode.Range(range.start.translate(deltaLines,0), range.end.translate(deltaLines,0)); 293 | // } 294 | // } 295 | 296 | public shiftRangeDelta(delta: textUtil.RangeDelta) : vscode.Range { 297 | if(this.ranges.length == 0 || (delta.linesDelta == 0 && delta.endCharactersDelta == 0)) 298 | return new vscode.Range(delta.start, delta.end); 299 | 300 | const firstIdx = this.findIndex(delta.start); 301 | let idx = firstIdx; 302 | 303 | if(delta.linesDelta != 0) { 304 | // shift all remaining lines 305 | for( ; idx < this.ranges.length; ++idx) 306 | this.ranges[idx] = textUtil.rangeTranslate(this.ranges[idx], delta); 307 | } else { 308 | // shift everything overlapping on the same end line 309 | for( ; idx < this.ranges.length && this.ranges[idx].start.line <= delta.end.line; ++idx) { 310 | this.ranges[idx] = textUtil.rangeTranslate(this.ranges[idx], delta); 311 | } 312 | } 313 | 314 | // not firstIdx <= idx <= this.ranges.length 315 | if(firstIdx == idx) 316 | return new vscode.Range(0,0,0,0); 317 | else if(idx == this.ranges.length) 318 | return new vscode.Range(this.ranges[firstIdx].start, this.ranges[idx-1].end); 319 | else 320 | return new vscode.Range(this.ranges[firstIdx].start, this.ranges[idx-1].end); 321 | } 322 | 323 | public shiftTextChange(target: vscode.Range, text: string) : vscode.Range { 324 | return this.shiftRangeDelta(textUtil.toRangeDelta(target, text)); 325 | } 326 | 327 | public getOverlapRanges(range: vscode.Range) : vscode.Range[] { 328 | const begin = this.findIndex(range.start); 329 | const end = this.findIndex(range.end); 330 | return this.ranges.slice(begin,end); 331 | } 332 | 333 | public getOverlap(range: vscode.Range, options: {excludeStart?: boolean, includeEnd?: boolean} = {excludeStart: false, includeEnd: false}) : DisjointRangeSet { 334 | const begin = this.findIndex(range.start, {excludeStart: options.excludeStart,includeEnd: range.isEmpty}); 335 | let end = this.findIndex(range.end, {excludeStart: true, includeEnd: options.includeEnd}); 336 | if(end < this.ranges.length && textUtil.rangeContains(this.ranges[end],range.end,!range.isEmpty,options.includeEnd)) 337 | ++end; 338 | return DisjointRangeSet.makeFromSortedRanges(this.ranges.slice(begin,end)); 339 | } 340 | 341 | /// shifts the line positions of ranges 342 | /// returns false if this operation failed because there is an overlapping 343 | /// range or if removing lines introduces an overlap 344 | // public shiftLines(pos: vscode.Position, deltaLines: number) : boolean { 345 | // if(this.ranges.length == 0 || deltaLines==0) 346 | // return true; 347 | 348 | // const idx = this.findIndex(pos); 349 | 350 | // if(idx >= this.ranges.length) 351 | // return true; 352 | // if(this.ranges[idx].contains(pos)) 353 | // return false; // overlapping conflict! 354 | 355 | // const prev = this.ranges[idx-1]; // may be undefined 356 | // const next = this.ranges[idx]; // exists 357 | 358 | // if(next.start.line+deltaLines < 0) 359 | // return false; 360 | // if(prev && prev.end.isAfter(next.start.translate(deltaLines,0))) 361 | // return false; 362 | 363 | // // shift the ranges after pos 364 | // while (idx < this.ranges.length) { 365 | // const range = this.ranges[idx]; 366 | // this.ranges[idx] = new vscode.Range(range.start.translate(deltaLines,0),range.end.translate(deltaLines,0)) 367 | // } 368 | // return true; 369 | // } 370 | } -------------------------------------------------------------------------------- /src/sregexp.ts: -------------------------------------------------------------------------------- 1 | // // /** 2 | // // * A library for supporting user-facings regular expressions. 3 | // // * * validates regular expression grammers 4 | // // * * compiles to RegExp 5 | // // * 6 | // // * Grammar: 7 | // // * C::= a-z | A-Z | 0-9 | ... Character sets 8 | // // * C::= . | \d | \D | \w | \W | \s | \S | \t | \r | \n | \v | \f | \0 | \cX | \xhh | \uhhhh | \u{hhhh} | \{hhhhh} 9 | // // * B::= ^ | $ | \b | \B Boundaries 10 | // // * Q::= E* | E+ | E? | E{n} | E{n,} | E{n,m} Quantifiers 11 | // // * E::= C 12 | // // * B 13 | // // * (E) 14 | // // * (?E) 15 | // // * (?:E) 16 | // // * \n Back reference 17 | // // * E|E union 18 | // // * EE concatination 19 | // // * Q 20 | // // * Q? 21 | // // * E(?=E) Only if folowwed by ... 22 | // // * E(?!E) If not followed by ... 23 | // // */ 24 | 25 | // type identity = number | string; 26 | 27 | // class Context { 28 | // constructor(public currentScope: string, public ids: identity[]) { } 29 | // public allocId(id: string) : Context { 30 | // if(/[.]/.test(id)) 31 | // throw new RegExpError(`invalid identity name: ${id}`); 32 | // if(this.ids.some((id2) => (id2==id))) 33 | // throw new RegExpError(`duplicate identity: ${id}`); 34 | // return new Context('', this.ids.concat(id)); 35 | // } 36 | // public static makeFresh() : Context { 37 | // return new Context('', []); 38 | // } 39 | // public getNextIndex() { 40 | // return this.ids.length; 41 | // } 42 | // public scope(scope: string) : Context { 43 | // if(/[.]/.test(scope)) 44 | // throw new RegExpError(`invalid scope name: ${scope}`); 45 | // return new Context(this.scope + '.' + scope, this.ids); 46 | // } 47 | // public concat(ctx: Context) { 48 | // ctx.ids.forEach((id1) => { 49 | // if(this.ids.some((id2) => (id1==id2))) 50 | // throw new RegExpError(`duplicate identity: ${id1}`); 51 | // }); 52 | // return new Context(ctx.currentScope, this.ids.concat(ctx.ids)); 53 | // } 54 | // public addIds(ids: identity[]) { 55 | // ids.forEach((id1) => { 56 | // if(typeof id1 == 'string' && !/[a-zA-Z]\w*/.test(id1)) 57 | // throw new RegExpError(`invalid identity name: ${id1}`); 58 | // if(this.ids.some((id2) => (id1==id2))) 59 | // throw new RegExpError(`duplicate identity: ${id1}`); 60 | // }); 61 | // return new Context(this.currentScope, this.ids.concat(ids)); 62 | // } 63 | // public allocFreshId() { 64 | // return {id: this.ids.length, newContext: new Context(this.currentScope, this.ids.concat(null))}; 65 | // } 66 | // public lookupBackreference(id: identity) { 67 | // const idx = this.ids.indexOf(id); 68 | // if(idx < 0) 69 | // throw new RegExpError(`backreference to ${id} is undefined`); 70 | // else 71 | // return 1+idx; 72 | // } 73 | // } 74 | 75 | // export class RegularExpression { 76 | // private regexp : RegExp; 77 | // interp: (registers: string[]) => T; 78 | // constructor(re : string, interp: (registers: string[]) => T) { 79 | // this.regexp = new RegExp(re); 80 | // this.interp = interp; 81 | // }; 82 | // public get source(): string { 83 | // return this.regexp.source; 84 | // } 85 | // public exec(str: string) : T { 86 | // const results = this.regexp.exec(str); 87 | // return this.interp(results); 88 | // } 89 | // } 90 | 91 | 92 | // class CompiledRE { 93 | // regexp: RegularExpression; 94 | // grouped: boolean; 95 | // newContext: Context; 96 | // ids: identity[]; // the identities provided by this expression 97 | // constructor(x: {regexp: RegularExpression, grouped: boolean, newContext: Context, ids: identity[]}) { 98 | // this.regexp=x.regexp; 99 | // this.grouped=x.grouped; 100 | // this.newContext=x.newContext; 101 | // this.ids = x.ids; 102 | // } 103 | // public with(x: {regexp?: RegularExpression, grouped?: boolean, newContext?: Context, ids?: identity[]}) : CompiledRE { 104 | // return new CompiledRE({regexp: x.regexp || this.regexp, grouped: x.grouped!==undefined ? x.grouped : this.grouped, newContext: x.newContext || this.newContext, ids: x.ids || this.ids}); 105 | // } 106 | // public withRegexp(x: {regexp: RegularExpression, grouped?: boolean, newContext?: Context, ids?: identity[]}) : CompiledRE { 107 | // return new CompiledRE({regexp: x.regexp, grouped: x.grouped!==undefined ? x.grouped : this.grouped, newContext: x.newContext || this.newContext, ids: x.ids || this.ids}); 108 | // } 109 | // } 110 | 111 | // class RegExpError { 112 | // constructor(public message: string) {} 113 | // } 114 | 115 | // export abstract class RegularExpressionAST { 116 | // abstract compileAST(context: Context) : CompiledRE; 117 | // abstract getIds() : identity[]; 118 | // public compile() : RegularExpression { 119 | // const cexp = this.compileAST(Context.makeFresh()); 120 | // return cexp.regexp; 121 | // } 122 | // } 123 | 124 | // function groupedRE(re: CompiledRE) : RegularExpression { 125 | // if(re.grouped) 126 | // return re.regexp; 127 | // else 128 | // return new RegularExpression(`(?:${re.regexp.source})`, re.regexp.interp); 129 | // } 130 | 131 | // function makeGrouped(re: CompiledRE) : CompiledRE { 132 | // if(re.grouped) 133 | // return re; 134 | // else 135 | // return re.with({ 136 | // regexp: new RegularExpression(`(?:${re.regexp.source})`, re.regexp.interp), 137 | // grouped: true 138 | // }); 139 | // } 140 | 141 | // // export class Union3> extends RegularExpressionAST<{index: number, value: T}> { 142 | // // constructor(private expressions : RegularExpressionAST[] = []) {super()} 143 | 144 | // // compileAST(context: Context) : CompiledRE<{index: number, value: T}> { 145 | // // const compiledExprs : CompiledRE[] = this.expressions.map((cexpr) => cexpr.compileAST(context)); 146 | // // const interp = (registers: string[]) : {index: number, value: T} => { 147 | // // for(let idx = 0; idx < compiledExprs.length; ++idx) { 148 | // // const results = compiledExprs[idx].regexp.interp(registers); 149 | // // if(results) 150 | // // return {index: idx, value: results}; 151 | // // } 152 | // // return undefined; 153 | // // }; 154 | // // const scopedIds = compiledExprs.reduce((ids: identity[], e, idx) => ids.concat(e.ids),[]); 155 | // // return new CompiledRE({ 156 | // // regexp: new RegularExpression<{index: number, value: T}>(compiledExprs.map((e) => groupedRE(e)).join('|'), interp), 157 | // // grouped: false, 158 | // // newContext: context.addIds(scopedIds), 159 | // // ids: scopedIds 160 | // // }); 161 | // // } 162 | // // getIds() : identity[] { 163 | // // return this.expressions.reduce((ids, e) => ids.concat(e.getIds()), []); 164 | // // } 165 | // // } 166 | 167 | // // const z = new Union3(); 168 | // // let a = z.compileAST() 169 | 170 | // export class Union extends RegularExpressionAST<{index: number, value: T}> { 171 | // constructor(private expressions : RegularExpressionAST[] = []) {super()} 172 | 173 | // compileAST(context: Context) : CompiledRE<{index: number, value: T}> { 174 | // const compiledExprs : CompiledRE[] = this.expressions.map((cexpr) => cexpr.compileAST(context)); 175 | // const interp = (registers: string[]) : {index: number, value: T} => { 176 | // for(let idx = 0; idx < compiledExprs.length; ++idx) { 177 | // const results = compiledExprs[idx].regexp.interp(registers); 178 | // if(results) 179 | // return {index: idx, value: results}; 180 | // } 181 | // return undefined; 182 | // }; 183 | // const scopedIds = compiledExprs.reduce((ids: identity[], e, idx) => ids.concat(e.ids),[]); 184 | // return new CompiledRE({ 185 | // regexp: new RegularExpression<{index: number, value: T}>(compiledExprs.map((e) => groupedRE(e)).join('|'), interp), 186 | // grouped: false, 187 | // newContext: context.addIds(scopedIds), 188 | // ids: scopedIds 189 | // }); 190 | // } 191 | // getIds() : identity[] { 192 | // return this.expressions.reduce((ids, e) => ids.concat(e.getIds()), []); 193 | // } 194 | // } 195 | 196 | 197 | // // export class Union2 extends RegularExpressionAST<{tag: I1, value: T1}|{tag: I2, value: T2}> { 198 | // // constructor(private tag1: I1, private expr1 : RegularExpressionAST, private tag2: I2, private expr2: RegularExpressionAST) {super()} 199 | // // compileAST(context: Context) : CompiledRE<{tag: I1, value: T1}|{tag: I2, value: T2}> { 200 | // // const cexpr1 = this.expr1.compileAST(context); 201 | // // const cexpr2 = this.expr2.compileAST(context); 202 | // // const interp = (registers: string[]) : {tag: I1, value: T1}|{tag: I2, value: T2} => { 203 | // // const results1 = cexpr1.regexp.interp(registers); 204 | // // if(results1) 205 | // // return {tag: this.tag1, value: results1}; 206 | // // else 207 | // // return {tag: this.tag2, value: cexpr2.regexp.interp(registers) }; 208 | // // }; 209 | // // return new CompiledRE({ 210 | // // regexp: new RegularExpression(groupedRE(cexpr1).source + '|' + groupedRE(cexpr2).source, interp), 211 | // // grouped: false, 212 | // // newContext: context.addIds(cexpr1.ids.concat(cexpr2.ids)), 213 | // // ids: cexpr1.ids.concat(cexpr2.ids), 214 | // // }); 215 | // // } 216 | // // getIds() : identity[] { 217 | // // return this.expr1.getIds().concat(this.expr2.getIds()); 218 | // // } 219 | // // } 220 | 221 | // // interface UnionTag { tag: T, value: V} 222 | 223 | // // export class Union, X2 extends UnionTag> extends RegularExpressionAST { 224 | // // constructor(private exprHd : RegularExpressionAST, private exprTl: RegularExpressionAST>) {super()} 225 | // // compileAST(context: Context) : CompiledRE<{0: Thd} & Array> { 226 | // // const cexpr1 = this.expr1.compileAST(context); 227 | // // const cexpr2 = this.expr2.compileAST(context); 228 | // // const interp = (registers: string[]) : {tag: I1, value: T1}|{tag: I2, value: T2} => { 229 | // // const results1 = cexpr1.regexp.interp(registers); 230 | // // if(results1) 231 | // // return {tag: this.tag1, value: results1}; 232 | // // else 233 | // // return {tag: this.tag2, value: cexpr2.regexp.interp(registers) }; 234 | // // }; 235 | // // return new CompiledRE({ 236 | // // regexp: new RegularExpression(groupedRE(cexpr1).source + '|' + groupedRE(cexpr2).source, interp), 237 | // // grouped: false, 238 | // // newContext: context.addIds(cexpr1.ids.concat(cexpr2.ids)), 239 | // // ids: cexpr1.ids.concat(cexpr2.ids), 240 | // // }); 241 | // // } 242 | // // getIds() : identity[] { 243 | // // return this.expr1.getIds().concat(this.expr2.getIds()); 244 | // // } 245 | // // } 246 | 247 | 248 | // // export class Union extends RegularExpressionAST<{0: Thd} & Array> { 249 | // // constructor(private exprHd : RegularExpressionAST, private exprTl: RegularExpressionAST>) {super()} 250 | // // compileAST(context: Context) : CompiledRE<{0: Thd} & Array> { 251 | // // const cexpr1 = this.expr1.compileAST(context); 252 | // // const cexpr2 = this.expr2.compileAST(context); 253 | // // const interp = (registers: string[]) : {tag: I1, value: T1}|{tag: I2, value: T2} => { 254 | // // const results1 = cexpr1.regexp.interp(registers); 255 | // // if(results1) 256 | // // return {tag: this.tag1, value: results1}; 257 | // // else 258 | // // return {tag: this.tag2, value: cexpr2.regexp.interp(registers) }; 259 | // // }; 260 | // // return new CompiledRE({ 261 | // // regexp: new RegularExpression(groupedRE(cexpr1).source + '|' + groupedRE(cexpr2).source, interp), 262 | // // grouped: false, 263 | // // newContext: context.addIds(cexpr1.ids.concat(cexpr2.ids)), 264 | // // ids: cexpr1.ids.concat(cexpr2.ids), 265 | // // }); 266 | // // } 267 | // // getIds() : identity[] { 268 | // // return this.expr1.getIds().concat(this.expr2.getIds()); 269 | // // } 270 | // // } 271 | 272 | // export class ScopedUnion extends RegularExpressionAST<{index: number, value: T}> { 273 | // constructor(private expressions : RegularExpressionAST[]) {super()} 274 | // compileAST(context: Context) : CompiledRE<{index: number, value: T}> { 275 | // const compiledExprs : CompiledRE[] = [] 276 | // for(let idx = 0; idx < this.expressions.length; ++idx) { 277 | // const cexpr = this.expressions[idx].compileAST(context.scope(idx.toString())); 278 | // compiledExprs.push(cexpr); 279 | // } 280 | // const interp = (registers: string[]) : {index: number, value: T} => { 281 | // for(let idx = 0; idx < compiledExprs.length; ++idx) { 282 | // const results = compiledExprs[idx].regexp.interp(registers); 283 | // if(results) 284 | // return {index: idx, value: results}; 285 | // } 286 | // return undefined; 287 | // }; 288 | // const scopedIds = compiledExprs.reduce((ids: identity[], e, idx) => ids.concat(e.ids),[]); 289 | // return new CompiledRE({ 290 | // regexp: new RegularExpression<{index: number, value: T}>(compiledExprs.map((e) => groupedRE(e)).join('|'), interp), 291 | // grouped: false, 292 | // newContext: context.addIds(scopedIds), 293 | // ids: scopedIds 294 | // }); 295 | // } 296 | // getIds() : identity[] { 297 | // return this.expressions.reduce((ids, e) => ids.concat(e.getIds()), []); 298 | // } 299 | // } 300 | 301 | // export class Concatenation> extends RegularExpressionAST { 302 | // constructor(private exprs : RegularExpressionAST[], private t: T) {super()} 303 | // compileAST(context: Context) : CompiledRE{ 304 | // const cexprs : CompiledRE[] = []; 305 | // let ctx = context; 306 | // for(const expr of this.exprs) { 307 | // const cexpr = expr.compileAST(ctx); 308 | // cexprs.push(cexpr); 309 | // ctx = cexpr.newContext; 310 | // } 311 | // const interp = (registers: string[]) : T => { 312 | // const results : T = this.t; 313 | // for(let idx = 0; idx < cexprs.length; ++idx) 314 | // results[idx] = cexprs[idx].regexp.interp(registers); 315 | // return results; 316 | // }; 317 | // return new CompiledRE({ 318 | // regexp: new RegularExpression(cexprs.map((e) => e.regexp.source).join(''), interp), 319 | // grouped: false, 320 | // newContext: ctx, 321 | // ids: cexprs.reduce((ids,e) => ids.concat(e.ids), []), 322 | // }); 323 | // } 324 | // getIds() : identity[] { 325 | // return this.exprs.reduce((ids,e) => ids.concat(e.getIds()), []); 326 | // } 327 | // } 328 | 329 | // type T = [number,string,number]; 330 | // const a : T = [1,'foo',3]; 331 | // const b : (RegularExpressionAST)[] = [null,null,null]; 332 | // const c = new Concatenation(b, a); 333 | // const x = c.compile(); 334 | // const z = x.exec('sss'); 335 | // const y= z[0]; 336 | 337 | 338 | // // export class Concatenation extends RegularExpressionAST { 339 | // // constructor(private expr1 : RegularExpressionAST, private expr2 : RegularExpressionAST) {super()} 340 | // // compileAST(context: Context) : CompiledRE{ 341 | // // const cexpr1 = this.expr1.compileAST(context); 342 | // // const cexpr2 = this.expr2.compileAST(cexpr1.newContext); 343 | // // const interp = (registers: string[]) : T1&T2 => { 344 | // // const results1 = cexpr1.regexp.interp(registers); 345 | // // const results2 = cexpr2.regexp.interp(registers); 346 | // // return Object.assign(results1, results2); 347 | // // }; 348 | // // return new CompiledRE({ 349 | // // regexp: new RegularExpression(groupedRE(cexpr1).source + '|' + groupedRE(cexpr2).source, interp), 350 | // // grouped: false, 351 | // // newContext: cexpr2.newContext, 352 | // // ids: cexpr1.ids.concat(cexpr2.ids), 353 | // // }); 354 | // // } 355 | // // getIds() : identity[] { 356 | // // return this.expr1.getIds().concat(this.expr2.getIds()); 357 | // // } 358 | // // } 359 | 360 | // let ctx = context; 361 | // const compiledExprs : CompiledRE[] = [] 362 | // for(const expr of this.expressions) { 363 | // const cexpr = expr.compile(ctx); 364 | // ctx = cexpr.newContext; 365 | // compiledExprs.push(cexpr); 366 | // } 367 | // return new CompiledRE({ 368 | // regexp: compiledExprs.map((e) => groupedRE(e)).join(''), 369 | // grouped: false, 370 | // newContext: ctx, 371 | // ids: compiledExprs.reduce((i,e) => i.concat(e.ids),[]) 372 | // }); 373 | // } 374 | // getIds() : identity[] { 375 | // return this.expressions.reduce((ids, e) => ids.concat(e.getIds()), []); 376 | // } 377 | // } 378 | 379 | // export class Quantifier extends RegularExpressionAST { 380 | // constructor(private expression : RegularExpressionAST, 381 | // private parameters : {minimum?: number, maximum?: number, greedy: boolean}) { 382 | // super(); 383 | // } 384 | // private compileGreedy() { 385 | // return (this.parameters.greedy === undefined || this.parameters.greedy === true) ? '' : '?' 386 | // } 387 | // private compileQuantifier() { 388 | // return `{${this.parameters.minimum || ''},${this.parameters.maximum || ''}`; 389 | // } 390 | // compile(context: Context) : CompiledRE { 391 | // const cexpr = this.expression.compile(context); 392 | // return new CompiledRE({ 393 | // regexp: `${groupedRE(cexpr)}${this.compileQuantifier()}${this.compileGreedy()}}`, 394 | // grouped: false, 395 | // newContext: cexpr.newContext, 396 | // ids: cexpr.ids 397 | // }); 398 | // } 399 | // getIds() : identity[] { 400 | // return this.expression.getIds(); 401 | // } 402 | // } 403 | 404 | // export class NoncapturingGroup extends RegularExpressionAST { 405 | // constructor(private expression : RegularExpressionAST) { 406 | // super(); 407 | // } 408 | // compile(context: Context) : CompiledRE { 409 | // return this.expression.compile(context); 410 | // } 411 | // getIds() : identity[] { 412 | // return this.expression.getIds(); 413 | // } 414 | // } 415 | 416 | // export class NamedCapturingGroup extends RegularExpressionAST { 417 | // constructor( 418 | // private expression : RegularExpressionAST, 419 | // private id: string) { 420 | // super(); 421 | // } 422 | // compile(context: Context) : CompiledRE { 423 | // const ctx = context.allocId(this.id); 424 | // const cexpr = this.expression.compile(ctx); 425 | // return new CompiledRE({ 426 | // regexp: `(${cexpr.regexp})`, 427 | // grouped: true, 428 | // newContext: cexpr.newContext, 429 | // ids: cexpr.ids.concat(this.id) 430 | // }); 431 | // } 432 | // getIds() : identity[] { 433 | // return this.expression.getIds().concat(this.id); 434 | // } 435 | // } 436 | 437 | 438 | // export class Backreference extends RegularExpressionAST { 439 | // constructor( 440 | // private id: identity) { 441 | // super(); 442 | // } 443 | // compile(context: Context) : CompiledRE { 444 | // return new CompiledRE({ 445 | // regexp: `\\${context.lookupBackreference(this.id)}`, 446 | // grouped: true, 447 | // newContext: context, 448 | // ids: [] 449 | // }); 450 | // } 451 | // getIds() : identity[] { 452 | // return []; 453 | // } 454 | // } 455 | -------------------------------------------------------------------------------- /src/PrettyModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 by Christian J. Bell 3 | * 4 | * PrettyModel.ts 5 | * 6 | * Models the substitutions within a text document 7 | */ 8 | import * as vscode from 'vscode'; 9 | import {Substitution, LanguageEntry, HideTextMethod} from './configuration'; 10 | import {RangeSet} from './RangeSet'; 11 | import {DisjointRangeSet} from './DisjointRangeSet'; 12 | //import * as drangeset from './DisjointRangeSet'; 13 | import * as textUtil from './text-util'; 14 | import * as tm from './text-mate'; 15 | import {MatchResult, iterateMatches, iterateMatchArray, mapIterator} from './regexp-iteration'; 16 | import * as decorations from './decorations'; 17 | 18 | const debugging = false; 19 | /* const activeEditorDecorationTimeout = 20; 20 | const inactiveEditorDecorationTimeout = 200; */ 21 | 22 | interface PrettySubstitution { 23 | ugly: RegExp, 24 | pretty: string, 25 | decorationType: vscode.TextEditorDecorationType, 26 | ranges: DisjointRangeSet, 27 | index: number, 28 | scope?: string, 29 | } 30 | 31 | export interface DocumentModel { 32 | getText: (r: vscode.Range) => string; 33 | getLine: (line: number) => string; 34 | getLineRange: (line: number) => vscode.Range; 35 | getLineCount: () => number; 36 | validateRange: (r: vscode.Range) => vscode.Range; 37 | } 38 | 39 | export interface UpdateDecorationEntry { 40 | decoration: vscode.TextEditorDecorationType, 41 | ranges: vscode.Range[], 42 | } 43 | 44 | export interface UpdateDecorationInstanceEntry { 45 | decoration: vscode.DecorationInstanceRenderOptions, 46 | ranges: DisjointRangeSet, 47 | } 48 | 49 | export class PrettyModel implements vscode.Disposable { 50 | private prettyDecorations : {scoped: PrettySubstitution[], unscoped: PrettySubstitution[]} = {scoped: [], unscoped: []}; 51 | /** matches all of the target substitutions that ignore grammar scopes */ 52 | private uglyUnscoped = new RegExp("","g"); 53 | /** condictional ranges: tracks ranges where any edit within the range should cause the entire range to be reparsed */ 54 | private conditionalRanges = new RangeSet(); 55 | /** ranges of hidden text */ 56 | private uglyDecorationRanges = new DisjointRangeSet(); 57 | // /** ranges of non-hidden, styled text */ 58 | // private styledDecorationRanges = new DisjointRangeSet(); 59 | /** things to dispose of at the end */ 60 | private subscriptions : vscode.Disposable[] = []; 61 | private changedUglies = false; // flag used to determine if the uglies have been updated 62 | 63 | // hides a "ugly" decorations 64 | private uglyDecoration: vscode.TextEditorDecorationType = null; 65 | // reveals the "ugly" decorations; is applied on top of uglyDecoration and should take priority 66 | private revealedUglyDecoration: vscode.TextEditorDecorationType = null; 67 | // draws a box around a pretty symbol 68 | private boxedSymbolDecoration: vscode.DecorationInstanceRenderOptions = null; 69 | 70 | // Stores the state for each line 71 | private grammarState : tm.StackElement[] = []; 72 | private grammar : null|tm.IGrammar = null; 73 | 74 | public constructor(doc: DocumentModel, settings: LanguageEntry, options: {hideTextMethod: HideTextMethod, textMateGrammar?: tm.IGrammar|null}, 75 | private document = doc, 76 | private revealStrategy = settings.revealOn, 77 | private prettyCursor = settings.prettyCursor, 78 | private hideTextMethod = options.hideTextMethod, 79 | private combineIdenticalScopes = settings.combineIdenticalScopes, 80 | ) { 81 | this.grammar = options.textMateGrammar || null; 82 | this.loadDecorations(settings.substitutions); 83 | 84 | // Parse whole document 85 | const docRange = new vscode.Range(0,0,this.document.getLineCount(),0); 86 | this.reparsePretties(docRange); 87 | } 88 | 89 | public dispose() { 90 | this.unloadDecorations(); 91 | this.debugDecorations.forEach((val) => val.dec.dispose()); 92 | this.subscriptions.forEach((s) => s.dispose()); 93 | } 94 | 95 | public getDecorationsList() : UpdateDecorationEntry[] { 96 | const decs : UpdateDecorationEntry[] = []; 97 | if(this.uglyDecoration) 98 | decs.push({decoration: this.uglyDecoration, ranges: this.uglyDecorationRanges.getRanges()}); 99 | for(const subst of this.prettyDecorations.unscoped) 100 | decs.push({decoration: subst.decorationType, ranges: subst.ranges.getRanges()}); 101 | for(const subst of this.prettyDecorations.scoped) 102 | decs.push({decoration: subst.decorationType, ranges: subst.ranges.getRanges()}); 103 | if(debugging) 104 | this.debugDecorations.forEach((val) => decs.push({decoration: val.dec, ranges: val.ranges})); 105 | 106 | return decs; 107 | } 108 | 109 | private unloadDecorations() { 110 | if(this.uglyDecoration) 111 | this.uglyDecoration.dispose(); 112 | if(this.revealedUglyDecoration) 113 | this.revealedUglyDecoration.dispose(); 114 | 115 | this.conditionalRanges = new RangeSet(); 116 | this.uglyDecorationRanges = new DisjointRangeSet(); 117 | // this.styledDecorationRanges = new DisjointRangeSet(); 118 | for(const subst of this.prettyDecorations.unscoped) 119 | subst.ranges = new DisjointRangeSet(); 120 | for(const subst of this.prettyDecorations.scoped) 121 | subst.ranges = new DisjointRangeSet(); 122 | this.debugDecorations.forEach((val) => val.ranges = []); 123 | 124 | for(const oldDecoration of this.prettyDecorations.unscoped) 125 | oldDecoration.decorationType.dispose(); 126 | for(const oldDecoration of this.prettyDecorations.scoped) 127 | oldDecoration.decorationType.dispose(); 128 | } 129 | 130 | private regexpOptionalGroup(re: string) { 131 | if(re) 132 | return `(?:${re})`; 133 | else 134 | return ""; 135 | } 136 | 137 | private loadDecorations(prettySubstitutions: Substitution[]) { 138 | this.unloadDecorations(); 139 | 140 | let dec : {uglyDecoration: vscode.TextEditorDecorationType, revealedUglyDecoration: vscode.TextEditorDecorationType, boxedSymbolDecoration: vscode.DecorationInstanceRenderOptions} 141 | if(this.hideTextMethod === "hack-fontSize") 142 | dec = decorations.makeDecorations_fontSize_hack(); 143 | else if(this.hideTextMethod === "hack-letterSpacing") 144 | dec = decorations.makeDecorations_letterSpacing_hack(); 145 | else 146 | dec = decorations.makeDecorations_none(); 147 | this.uglyDecoration = dec.uglyDecoration; 148 | this.revealedUglyDecoration = dec.revealedUglyDecoration; 149 | this.boxedSymbolDecoration = dec.boxedSymbolDecoration; 150 | 151 | this.prettyDecorations.scoped = []; 152 | this.prettyDecorations.unscoped = []; 153 | const uglyAllUnscopedStrings = []; 154 | for(const prettySubst of prettySubstitutions) { 155 | const pre = (prettySubst.scope && prettySubst.pre===undefined) ? "^" : this.regexpOptionalGroup(prettySubst.pre); 156 | const post = (prettySubst.scope && prettySubst.post===undefined) ? "$" : this.regexpOptionalGroup(prettySubst.post); 157 | const uglyStr = pre + "(" + prettySubst.ugly + ")" + post; 158 | try { 159 | const re = new RegExp(uglyStr, "g"); 160 | if(re.test("")) { 161 | console.warn(`Substitution ignored because it matches the empty string: "${uglyStr}" --> "${prettySubst.pretty}"`); 162 | continue; 163 | } 164 | 165 | let decoration = undefined; 166 | if(!prettySubst.pretty) 167 | decoration = decorations.makePrettyDecoration_noPretty(prettySubst); 168 | else if(this.hideTextMethod === "hack-fontSize") 169 | decoration = decorations.makePrettyDecoration_fontSize_hack(prettySubst); 170 | else if(this.hideTextMethod === "hack-letterSpacing") 171 | decoration = decorations.makePrettyDecoration_letterSpacing_hack(prettySubst); 172 | else 173 | decoration = decorations.makePrettyDecoration_noHide(prettySubst); 174 | 175 | if(prettySubst.scope) { 176 | this.prettyDecorations.scoped.push({ 177 | ugly: re, 178 | pretty: prettySubst.pretty, 179 | ranges: new DisjointRangeSet(), 180 | decorationType: decoration, 181 | index: this.prettyDecorations.scoped.length, 182 | scope: prettySubst.scope, 183 | }); 184 | } else { 185 | this.prettyDecorations.unscoped.push({ 186 | ugly: re, 187 | pretty: prettySubst.pretty, 188 | ranges: new DisjointRangeSet(), 189 | decorationType: decoration, 190 | index: this.prettyDecorations.scoped.length, 191 | }); 192 | uglyAllUnscopedStrings.push(`(?:${uglyStr})`); 193 | } 194 | 195 | } catch(e) { 196 | console.warn(`Could not add rule "${uglyStr}" --> "${prettySubst.pretty}"; invalid regular expression`) 197 | } 198 | } 199 | this.uglyUnscoped = new RegExp(uglyAllUnscopedStrings.join('|'), 'g'); 200 | } 201 | 202 | // helper function to determine which ugly has been matched 203 | private getUglyFromMatch(match: RegExpExecArray, line: number) { 204 | const matches = match 205 | .map((value,idx) => ({index:idx,match:value})) 206 | .filter((value) => value.match !== undefined); 207 | if(matches.length <= 1) 208 | return undefined; 209 | const matchIdx = matches[matches.length-1].index; 210 | const matchStr = match[matchIdx]; 211 | const matchStart = match.index; 212 | //const matchEnd = matchStart + match[0].length; 213 | const start = matchStart + match[0].indexOf(matchStr); 214 | const end = start + matchStr.length; 215 | const uglyRange = new vscode.Range(line,start,line,end); 216 | 217 | return {range: uglyRange, prettyIndex: matchIdx-1, lastIndex: end}; 218 | } 219 | 220 | private refreshTokensOnLine(line: string, lineNumber: number) : {tokens: tm.IToken[], invalidated: boolean} { 221 | if(!this.grammar) 222 | return {tokens: [], invalidated: false}; 223 | try { 224 | const prevState = this.grammarState[lineNumber-1] || null; 225 | const lineTokens = this.grammar.tokenizeLine(line, prevState); 226 | const invalidated = !this.grammarState[lineNumber] || !lineTokens.ruleStack.equals(this.grammarState[lineNumber]) 227 | this.grammarState[lineNumber] = lineTokens.ruleStack; 228 | return {tokens: lineTokens.tokens, invalidated: invalidated}; 229 | } catch (error) { 230 | return {tokens: [], invalidated: false}; 231 | } 232 | } 233 | 234 | /** Iterates over all the uglies that match within a scoped token 235 | * @returns an iterator; `next` accepts a new offset within the string to jump to 236 | */ 237 | private *iterateScopedUglies(line: string, tokens: tm.IToken[]) : IterableIterator { 238 | let jumpOffset : number|undefined = undefined; 239 | nextToken: 240 | for(let tokenIdx = 0; tokenIdx < tokens.length; ++tokenIdx) { 241 | const token = tokens[tokenIdx]; 242 | if(token.startIndex < jumpOffset || token.endIndex===token.startIndex) 243 | continue nextToken; // advance to the next offset we're interested in 244 | const tokenStr = line.substring(token.startIndex,token.endIndex); 245 | const matchScopes = this.prettyDecorations.scoped 246 | .filter(s => tm.matchScope(s.scope, token.scopes)); 247 | const matchIter = iterateMatchArray(tokenStr, matchScopes.map(ms => ms.ugly)) 248 | let match = matchIter.next(); 249 | for(; !match.done; match = matchIter.next()) { 250 | const newOffset = yield { 251 | start: token.startIndex + match.value.start, 252 | end: token.startIndex + match.value.end, 253 | matchStart: token.startIndex, 254 | matchEnd: token.endIndex, 255 | id: matchScopes[match.value.id].index}; 256 | if(typeof newOffset === 'number') { 257 | jumpOffset = newOffset; 258 | if(newOffset < token.startIndex) // start over and search for the correct token 259 | tokenIdx = -1; 260 | } 261 | } 262 | } 263 | } 264 | 265 | private *iterateUnscopedUglies(line: string) : IterableIterator { 266 | yield *iterateMatches(line, this.uglyUnscoped); 267 | } 268 | 269 | 270 | 271 | private *iterateLineUglies(line: string, tokens: tm.IToken[]) : IterableIterator { 272 | type T = "scoped" | "unscoped"; 273 | //let offset = 0; 274 | //const tokensOld = tokens; 275 | if(this.combineIdenticalScopes) 276 | tokens = tm.combineIdenticalTokenScopes(tokens); 277 | const scopedUgliesIter = this.iterateScopedUglies(line, tokens); 278 | const unscopedUgliesIter = this.iterateUnscopedUglies(line); 279 | let matchScoped = scopedUgliesIter.next(); 280 | let matchUnscoped = unscopedUgliesIter.next(); 281 | while(!matchScoped.done && !matchUnscoped.done) { 282 | const s = matchScoped.value; 283 | const u = matchUnscoped.value; 284 | if(s.end <= u.start) {// process scoped; sctrictly first 285 | yield Object.assign(s, {type: "scoped" as T}); 286 | matchScoped = scopedUgliesIter.next(); 287 | } else if(u.end <= s.start) {// process unscoped; strictly first 288 | yield Object.assign(u, {type: "unscoped" as T}); 289 | matchUnscoped = unscopedUgliesIter.next(); 290 | } else {// overlap: consume the scoped ugly and discard the unscoped ugly 291 | yield Object.assign(s, {type: "scoped" as T}); 292 | matchScoped = scopedUgliesIter.next(); 293 | matchUnscoped = unscopedUgliesIter.next(s.end /* discard current match and start looking after the scoped match */); 294 | } 295 | } 296 | if(!matchScoped.done) 297 | yield *mapIterator(scopedUgliesIter, (x) => Object.assign(x, {type: "scoped" as T}), matchScoped) 298 | else if(!matchUnscoped.done) 299 | yield *mapIterator(unscopedUgliesIter, (x) => Object.assign(x, {type: "unscoped" as T}), matchUnscoped) 300 | } 301 | 302 | /** Reparses the given range; assumes that the range has already been cleared by `clearPretties` 303 | * Updates: 304 | * this.prettyDecorations.scoped[x].ranges -- adds new ranges for each pretty x encountered 305 | * this.prettyDecorations.unscoped[x].ranges -- adds new ranges for each pretty x encountered 306 | * this.uglyDecorationRanges -- all new uglies [to be hidden] are added 307 | * @returns the range that was acutally reparsed 308 | */ 309 | private reparsePretties(range: vscode.Range) : vscode.Range { 310 | range = this.document.validateRange(range); 311 | const startCharacter = 0; 312 | 313 | const newUglyRanges = new DisjointRangeSet(); 314 | const newStyledRanges = new DisjointRangeSet(); 315 | const newScopedRanges : DisjointRangeSet[] = []; 316 | const newUnscopedRanges : DisjointRangeSet[] = []; 317 | const newConditionalRanges = new RangeSet(); 318 | // initialize an empty range set for every id 319 | this.prettyDecorations.unscoped.forEach(() => newUnscopedRanges.push(new DisjointRangeSet())); 320 | this.prettyDecorations.scoped.forEach(() => newScopedRanges.push(new DisjointRangeSet())); 321 | 322 | let invalidatedTokenState = false; 323 | 324 | // Collect new pretties 325 | const lineCount = this.document.getLineCount(); 326 | let lineIdx; 327 | for(lineIdx = range.start.line; lineIdx <= range.end.line || (invalidatedTokenState && lineIdx < lineCount); ++lineIdx) { 328 | const line = this.document.getLine(lineIdx); 329 | const {tokens: tokens, invalidated: invalidated} = this.refreshTokensOnLine(line, lineIdx); 330 | invalidatedTokenState = invalidated; 331 | 332 | for(const ugly of this.iterateLineUglies(line, tokens)) { 333 | const uglyRange = new vscode.Range(lineIdx, ugly.start, lineIdx, ugly.end); 334 | newConditionalRanges.add(new vscode.Range(lineIdx, ugly.matchStart, lineIdx, ugly.matchEnd)); 335 | if(ugly.type === "scoped") { 336 | if(this.prettyDecorations.scoped[ugly.id].pretty) 337 | newUglyRanges.insert(uglyRange); 338 | else 339 | newStyledRanges.insert(uglyRange); 340 | newScopedRanges[ugly.id].insert(uglyRange); 341 | } else if(ugly.type === "unscoped") { 342 | if(this.prettyDecorations.unscoped[ugly.id].pretty) 343 | newUglyRanges.insert(uglyRange); 344 | else 345 | newStyledRanges.insert(uglyRange); 346 | newUnscopedRanges[ugly.id].insert(uglyRange); 347 | } 348 | } 349 | } 350 | if(lineIdx-1 > range.end.line) { 351 | // console.info('Aditional tokens reparsed: ' + (lineIdx-range.end.line) + ' lines'); 352 | range = range.with({end: range.end.with({line: lineIdx, character: 0})}); 353 | } 354 | 355 | // compute the total reparsed range 356 | // use this to clear any preexisting substitutions 357 | const newUglyTotalRange = newUglyRanges.getTotalRange(); 358 | const newStyledTotalRange = newStyledRanges.getTotalRange(); 359 | let hiddenOverlap = range.with({start:range.start.with({character: startCharacter})}); 360 | let styledOverlap = range.with({start:range.start.with({character: startCharacter})}); 361 | if(!newUglyTotalRange.isEmpty) 362 | hiddenOverlap = hiddenOverlap.union(newUglyRanges.getTotalRange()); 363 | if(!newStyledTotalRange.isEmpty) 364 | styledOverlap =styledOverlap.union(newStyledRanges.getTotalRange()); 365 | const overlap = hiddenOverlap.union(styledOverlap); 366 | 367 | this.conditionalRanges.removeOverlapping(overlap, {includeTouchingStart: false, includeTouchingEnd: false}); 368 | this.uglyDecorationRanges.removeOverlapping(hiddenOverlap); 369 | // this.styledDecorationRanges.removeOverlapping(styledOverlap); 370 | this.prettyDecorations.unscoped.forEach(r => r.ranges.removeOverlapping(overlap)); 371 | this.prettyDecorations.scoped.forEach(r => r.ranges.removeOverlapping(overlap)); 372 | 373 | // add the new pretties & ugly ducklings 374 | newConditionalRanges.getRanges().forEach(r => this.conditionalRanges.add(r)); 375 | this.uglyDecorationRanges.insertRanges(newUglyRanges); 376 | this.prettyDecorations.unscoped.forEach((pretty,idx) => pretty.ranges.insertRanges(newUnscopedRanges[idx])); 377 | this.prettyDecorations.scoped.forEach((pretty,idx) => { 378 | pretty.ranges.insertRanges(newScopedRanges[idx]) 379 | }); 380 | 381 | if(!newStyledRanges.isEmpty() || !newUglyRanges.isEmpty()) 382 | this.changedUglies = true; 383 | return hiddenOverlap.union(styledOverlap); 384 | } 385 | 386 | private debugDecorations : {dec:vscode.TextEditorDecorationType, ranges: vscode.Range[]}[] = 387 | [ {dec: vscode.window.createTextEditorDecorationType({textDecoration: 'line-through'}), ranges: []} // affected uglies 388 | , {dec: vscode.window.createTextEditorDecorationType({backgroundColor: 'yellow',}), ranges: []} // reparsed text 389 | , {dec: vscode.window.createTextEditorDecorationType({outlineColor: 'black', outlineStyle: 'solid', outlineWidth: '1pt'}), ranges: []} // editRange 390 | ]; 391 | 392 | /** 393 | * @returns true if the decorations were invalidated/updated 394 | */ 395 | public applyChanges(changes: {range: vscode.Range, text: string}[]) : boolean { 396 | // this.cachedLines = []; 397 | if(debugging) 398 | this.debugDecorations.forEach((val) => val.ranges = []); 399 | // const startTime = new Date().getTime(); 400 | this.changedUglies = false; // assume no changes need to be made for now 401 | const sortedChanges = 402 | changes.sort((change1,change2) => change1.range.start.isAfter(change2.range.start) ? -1 : 1) 403 | const adjustedReparseRanges = new RangeSet(); 404 | for(const change of sortedChanges) { 405 | try { 406 | const delta = textUtil.toRangeDelta(change.range, change.text); 407 | const editRange = textUtil.rangeDeltaNewRange(delta); 408 | 409 | adjustedReparseRanges.shiftDelta(delta); 410 | 411 | const reparseRanges = this.conditionalRanges.removeOverlapping(change.range,{includeTouchingStart:true,includeTouchingEnd:true}); 412 | this.conditionalRanges.shiftDelta(delta); 413 | const reparseRange = reparseRanges.length > 0 414 | ? new vscode.Range(reparseRanges[0].start, reparseRanges[reparseRanges.length-1].end) 415 | : change.range; 416 | // note: take the union to make sure that each edit location is reparsed, even if there were no preeexisting uglies (i.e. allow searching for new uglies) 417 | adjustedReparseRanges.add(textUtil.rangeTranslate(reparseRange, delta).union(editRange)); 418 | 419 | const removed = this.uglyDecorationRanges.removeOverlapping(reparseRange,{includeTouchingStart:true,includeTouchingEnd:true}); 420 | const affected = this.uglyDecorationRanges.shiftRangeDelta(delta); 421 | if(removed.length > 0) 422 | this.changedUglies = true; 423 | 424 | for(const subst of this.prettyDecorations.unscoped) { 425 | subst.ranges.removeOverlapping(reparseRange,{includeTouchingStart:true,includeTouchingEnd:true}); 426 | subst.ranges.shiftRangeDelta(delta); 427 | } 428 | for(const subst of this.prettyDecorations.scoped) { 429 | subst.ranges.removeOverlapping(reparseRange,{includeTouchingStart:true,includeTouchingEnd:true}); 430 | subst.ranges.shiftRangeDelta(delta); 431 | } 432 | 433 | if(debugging) { 434 | this.debugDecorations[0].ranges.push(affected); 435 | this.debugDecorations[2].ranges.push(reparseRange); 436 | } 437 | } catch(e) { 438 | console.error(e); 439 | } 440 | } 441 | 442 | for(const range of adjustedReparseRanges.getRanges()) { 443 | const reparsed = this.reparsePretties(range); 444 | this.debugDecorations[1].ranges.push(reparsed); 445 | } 446 | 447 | 448 | // else if(debugging) 449 | // this.debugDecorations.forEach((val) => this.getEditors().forEach((e) => e.setDecorations(val.dec,val.ranges))); 450 | 451 | // this.refresh(); 452 | // const endTime = new Date().getTime(); 453 | // console.log(endTime - startTime + "ms") 454 | 455 | return this.changedUglies; 456 | } 457 | 458 | /** reparses the document and recreates the highlights for all editors */ 459 | public recomputeDecorations() { 460 | this.uglyDecorationRanges = new DisjointRangeSet(); 461 | this.grammarState = []; 462 | for(const subst of this.prettyDecorations.unscoped) 463 | subst.ranges = new DisjointRangeSet(); 464 | for(const subst of this.prettyDecorations.scoped) 465 | subst.ranges = new DisjointRangeSet(); 466 | this.debugDecorations.forEach((val) => val.ranges = []) 467 | 468 | const docRange = new vscode.Range(0,0,this.document.getLineCount(),0); 469 | this.reparsePretties(docRange); 470 | } 471 | 472 | private findSymbolAt(pos: vscode.Position, options: {excludeStart?: boolean, includeEnd?: boolean} = {excludeStart: false, includeEnd: false}) { 473 | return this.uglyDecorationRanges.find(pos,options); 474 | } 475 | 476 | private findSymbolsIn(range: vscode.Range) { 477 | return this.uglyDecorationRanges.getOverlap(range); 478 | } 479 | 480 | public getPrettySubstitutionsRanges() : vscode.Range[] { 481 | return this.uglyDecorationRanges.getRanges(); 482 | } 483 | 484 | /** 485 | * Returns what the contents of the document would appear to be after decorations (i.e. with substitutions applied to the text) 486 | */ 487 | public getDecoratedText(range : vscode.Range) : string { 488 | range = this.document.validateRange(range); 489 | 490 | const text = this.document.getText(range); 491 | const substitutions : {start: number, end: number, subst: string}[] = [] 492 | 493 | for(const subst of this.prettyDecorations.unscoped) { 494 | if(!subst.pretty) 495 | continue; 496 | const substRanges = subst.ranges.getOverlapRanges(range); 497 | for(const sr of substRanges) { 498 | const start = textUtil.relativeOffsetAtAbsolutePosition(text, range.start, sr.start); 499 | const end = textUtil.relativeOffsetAtAbsolutePosition(text, range.start, sr.end); 500 | substitutions.push({start: start, end: end, subst: subst.pretty}) 501 | } 502 | } 503 | for(const subst of this.prettyDecorations.scoped) { 504 | if(!subst.pretty) 505 | continue; 506 | const substRanges = subst.ranges.getOverlapRanges(range); 507 | for(const sr of substRanges) { 508 | const start = textUtil.relativeOffsetAtAbsolutePosition(text, range.start, sr.start); 509 | const end = textUtil.relativeOffsetAtAbsolutePosition(text, range.start, sr.end); 510 | substitutions.push({start: start, end: end, subst: subst.pretty}) 511 | } 512 | } 513 | 514 | // reverse order: later substs first 515 | const sortedSubst = substitutions.sort((a,b) => a.start < b.start ? 1 : a.start === b.start ? 0 : -1); 516 | 517 | let result = text; 518 | for(const subst of sortedSubst) { 519 | result = result.slice(0,subst.start) + subst.subst + result.slice(subst.end); 520 | } 521 | 522 | return result 523 | } 524 | 525 | public revealSelections(selections: vscode.Selection[]) : UpdateDecorationEntry { 526 | const revealUgly = (getRange: (sel:vscode.Selection) => vscode.Range) : UpdateDecorationEntry => { 527 | const cursorRevealedRanges = new DisjointRangeSet(); 528 | for(const selection of selections) { 529 | const ugly = getRange(selection); 530 | if(ugly) 531 | cursorRevealedRanges.insert(ugly); 532 | } 533 | // reveal the uglies and hide the pretties 534 | return {decoration: this.revealedUglyDecoration, ranges: cursorRevealedRanges.getRanges()}; 535 | } 536 | const revealUglies = (getRanges: (sel:vscode.Selection) => DisjointRangeSet) : UpdateDecorationEntry => { 537 | const cursorRevealedRanges = new DisjointRangeSet(); 538 | for(const selection of selections) { 539 | const ugly = getRanges(selection); 540 | if(ugly) 541 | cursorRevealedRanges.insertRanges(ugly); 542 | } 543 | // reveal the uglies and hide the pretties 544 | return {decoration: this.revealedUglyDecoration, ranges: cursorRevealedRanges.getRanges()}; 545 | } 546 | 547 | // add the new intersections 548 | switch(this.revealStrategy) { 549 | case 'cursor': 550 | return revealUgly((sel) => this.findSymbolAt(sel.active,{includeEnd: true})); 551 | case 'cursor-inside': 552 | return revealUgly((sel) => this.findSymbolAt(sel.active,{excludeStart: true})); 553 | case 'active-line': 554 | return revealUglies((sel) => this.findSymbolsIn(this.document.getLineRange(sel.active.line))); 555 | case 'selection': 556 | return revealUglies((sel) => this.findSymbolsIn(new vscode.Range(sel.start, sel.end))); 557 | default: 558 | return {decoration: this.revealedUglyDecoration, ranges: []}; 559 | } 560 | } 561 | 562 | public renderPrettyCursor(selections: vscode.Selection[]) : UpdateDecorationInstanceEntry|null { 563 | switch(this.prettyCursor) { 564 | case 'boxed': { 565 | const boxPretty = (getRange: (sel:vscode.Selection) => vscode.Range) : UpdateDecorationInstanceEntry|null => { 566 | try { 567 | const cursorBoxRanges = new DisjointRangeSet(); 568 | for(const selection of selections) { 569 | const pretty = getRange(selection); 570 | if(pretty) 571 | cursorBoxRanges.insert(pretty); 572 | } 573 | // reveal the uglies and hide the pretties 574 | return {decoration: this.boxedSymbolDecoration, ranges: cursorBoxRanges}; 575 | } catch(err) { 576 | console.error(err); 577 | console.error('\n'); 578 | return null; 579 | } 580 | } 581 | return boxPretty((sel) => this.findSymbolAt(sel.active)); 582 | } 583 | default: 584 | return null; 585 | } 586 | } 587 | 588 | } --------------------------------------------------------------------------------