import * as vscode from 'vscode' ; import * as copyPaste from 'copy-paste' ; import { Substitution , UglyRevelation , LanguageEntry , PrettyCursor , PrettyStyleProperties , PrettyStyle , assignStyleProperties , HideTextMethod } from './configuration' ; import * as pos from './position' ; import { RangeSet } from './RangeSet' ; import { DisjointRangeSet } from './DisjointRangeSet' ; import * as drangeset from './DisjointRangeSet' ; import * as textUtil from './text-util' ; import * as tm from './text-mate' ; import { MatchResult , iterateMatches , iterateMatchArray , mapIterator } from './regexp-iteration' ; import * as decorations from './decorations' ; import { PrettyModel , UpdateDecorationEntry , UpdateDecorationInstanceEntry } from './PrettyModel' ; const debugging = false ; const activeEditorDecorationTimeout = 100 ; const updateSelectionTimeout = 20 ; const inactiveEditorDecorationTimeout = 500 ; function arrayEqual < T > ( a1 , a2 , isEqual = ( ( x , y ) => x === y ) ) { if ( a1 . length != a2 . length ) return false ; for ( let idx = 0 ; idx < a1 . length ; ++ idx ) { if ( ! isEqual ( a1 [ idx ] , a2 [ idx ] ) ) return false ; } return true ; } class DebounceFunction implements vscode . Disposable { private timer ? : NodeJS . Timer = null ; private callback ? : ( ) => void = null ; constructor ( private timeout ) { } public dispose ( ) { if ( this . timer != null ) { clearTimeout ( this . timer ) ; this . timer = null ; } } public call ( callback ) { this . callback = callback ; if ( this . timer == null ) { this . timer = setTimeout ( ( ) => { this . callback ( ) ; this . callback = null ; this . timer = null ; } , this . timeout ) ; } } } export class PrettyDocumentController implements vscode . Disposable { private readonly model : PrettyModel ; private readonly subscriptions : vscode . Disposable [ ] = [ ] ; private currentDecorations : UpdateDecorationEntry [ ] = [ ] ; private updateActiveEditor = new DebounceFunction ( activeEditorDecorationTimeout ) ; private updateInactiveEditors = new DebounceFunction ( inactiveEditorDecorationTimeout ) ; private updateSelection = new DebounceFunction ( updateSelectionTimeout ) ; public constructor ( doc , settings , options , private document = doc , private adjustCursorMovement = settings . adjustCursorMovement ) { const docModel = { getText : ( r ? ) => this . document . getText ( r ) , getLine : ( n ) => this . document . lineAt ( n ) . text , getLineRange : ( n ) => this . document . lineAt ( n ) . range , getLineCount : ( ) => this . document . lineCount , validateRange : ( r ) => this . document . validateRange ( r ) , } ; this . model = new PrettyModel ( docModel , settings , options ) ; this . subscriptions . push ( vscode . workspace . onDidChangeTextDocument ( ( e ) => { if ( e . document == this . document ) this . onChangeDocument ( e ) ; } ) ) ; this . applyDecorations ( this . getEditors ( ) , this . model . getDecorationsList ( ) ) ; } public dispose ( ) { this . model . dispose ( ) ; this . subscriptions . forEach ( ( s ) => s . dispose ( ) ) ; } private getEditors ( ) { return vscode . window . visibleTextEditors . filter ( ( editor ) => { return editor . document . uri === this . document . uri ; } ) ; } public gotFocus ( editor ) { this . applyDecorations ( this . getEditors ( ) , this . currentDecorations ) ; } public copyDecorated ( editor ) { function doCopy ( x ) { return new Promise < void > ( ( resolve , reject ) => copyPaste . copy ( x , ( err ) => err ? reject ( err ) : resolve ( ) ) ) ; } const copy = editor . selections . map ( sel => this . model . getDecoratedText ( sel ) ) ; if ( copy . length === 0 ) return Promise . resolve ( ) ; else return doCopy ( copy . join ( '\n' ) ) ; } private applyActiveEditorDecorations ( editors , decs , revealRanges ? , prettyCursors ? ) { this . updateActiveEditor . call ( ( ) => { try { for ( const editor of editors ) { const cursors = prettyCursors || this . model . renderPrettyCursor ( editor . selections ) ; const reveal = revealRanges || this . model . revealSelections ( editor . selections ) . ranges ; decs . forEach ( d => editor . setDecorations ( d . decoration , d . ranges . filter ( r => reveal . every ( s => s . intersection ( r ) === undefined ) ) . map ( r => ( { range : r , renderOptions : cursors && cursors . ranges . isOverlapping ( r ) ? cursors . decoration : undefined } ) ) ) ) ; } } catch ( err ) { console . error ( err ) ; } } ) ; } private applyInactiveEditorDecorations ( editors , decs ) { this . updateInactiveEditors . call ( ( ) => { try { for ( const editor of editors ) { if ( editor === vscode . window . activeTextEditor ) continue ; decs . forEach ( d => editor . setDecorations ( d . decoration , d . ranges ) ) ; } } catch ( err ) { console . error ( err ) ; } } ) ; } private applyDecorations ( editors , decs ) { this . currentDecorations = decs ; this . applyActiveEditorDecorations ( [ vscode . window . activeTextEditor ] , decs ) ; this . applyInactiveEditorDecorations ( editors , decs ) ; } private onChangeDocument ( event ) { if ( this . model . applyChanges ( event . contentChanges ) ) this . applyDecorations ( this . getEditors ( ) , this . model . getDecorationsList ( ) ) ; } public refresh ( ) { this . model . recomputeDecorations ( ) ; this . applyDecorations ( this . getEditors ( ) , this . model . getDecorationsList ( ) ) ; } private lastSelections = new Map < vscode . TextEditor , vscode . Selection [ ] > ( ) ; public adjustCursor ( editor ) { let updated = false ; let adjustedSelections = [ ] ; let before = this . lastSelections . get ( editor ) ; if ( ! before ) { this . lastSelections . set ( editor , editor . selections ) ; return editor . selections ; } const after = editor . selections ; if ( arrayEqual ( before , after ) ) return null ; after . forEach ( ( sel , idx ) => { if ( before [ idx ] === undefined ) { adjustedSelections . push ( new vscode . Selection ( sel . anchor , sel . active ) ) ; return ; } const adjusted = pos . adjustCursorMovement ( before [ idx ] . active , sel . active , this . document , this . model . getPrettySubstitutionsRanges ( ) ) ; if ( ! adjusted . pos . isEqual ( sel . active ) ) { updated = true ; } if ( sel . anchor . isEqual ( sel . active ) ) adjustedSelections . push ( new vscode . Selection ( adjusted . pos , adjusted . pos ) ) ; else adjustedSelections . push ( new vscode . Selection ( sel . anchor , adjusted . pos ) ) ; } ) ; this . lastSelections . set ( editor , adjustedSelections ) ; if ( updated ) editor . selections = adjustedSelections ; return adjustedSelections ; } private revealedRanges : vscode . Range [ ] = [ ] ; private cursorRanges : vscode . Range [ ] = [ ] ; param editor public selectionChanged ( editor ) { this . updateSelection . call ( ( ) => { let selections ; if ( this . adjustCursorMovement ) { selections = this . adjustCursor ( editor ) ; } else { selections = editor . selections ; } if ( selections == null ) { return ; } const cursors = this . model . renderPrettyCursor ( selections ) ; const cR = cursors == null ? [ ] : cursors . ranges . getRanges ( ) ; const revealed = this . model . revealSelections ( selections ) ; if ( ! arrayEqual ( revealed . ranges , this . revealedRanges ) || ! arrayEqual ( cR , this . cursorRanges ) ) { this . applyActiveEditorDecorations ( [ editor ] , this . model . getDecorationsList ( ) , revealed . ranges , cursors ) ; } this . revealedRanges = revealed . ranges ; this . cursorRanges = cR ; } ) ; } }