@ -1,9 +1,9 @@
'use strict' ;
import { Functions , IDeferred } from '../system' ;
import { ConfigurationChangeEvent , Disposable , Event , EventEmitter , TextDocumentChangeEvent , TextEditor , window , workspace } from 'vscode' ;
import { ConfigurationChangeEvent , Disposable , Event , EventEmitter , Range , TextDocumentChangeEvent, TextEditor , TextEditorSelectionChangeEvent , window , workspace } from 'vscode' ;
import { TextDocumentComparer } from '../comparers' ;
import { configuration } from '../configuration' ;
import { CommandContext , isTextEditor , setCommandContext } from '../constants' ;
import { CommandContext , isTextEditor , RangeEndOfLineIndex , setCommandContext } from '../constants' ;
import { GitChangeEvent , GitChangeReason , GitService , GitUri , Repository , RepositoryChangeEvent } from '../gitService' ;
import { Logger } from '../logger' ;
@ -15,11 +15,24 @@ export enum BlameabilityChangeReason {
}
export interface BlameabilityChangeEvent {
blameable : boolean ;
editor : TextEditor | undefined ;
blameable : boolean ;
dirty : boolean ;
reason : BlameabilityChangeReason ;
}
export interface DirtyStateChangeEvent {
editor : TextEditor | undefined ;
dirty : boolean ;
}
export interface LineDirtyStateChangeEvent extends DirtyStateChangeEvent {
line : number ;
lineDirty : boolean ;
}
interface Context {
editor? : TextEditor ;
repo? : Repository ;
@ -31,6 +44,8 @@ interface Context {
interface ContextState {
blameable? : boolean ;
dirty : boolean ;
line? : number ;
lineDirty? : boolean ;
revision? : boolean ;
tracked? : boolean ;
}
@ -42,17 +57,35 @@ export class GitContextTracker extends Disposable {
return this . _onDidChangeBlameability . event ;
}
private _onDidChangeDirtyState = new EventEmitter < DirtyStateChangeEvent > ( ) ;
get onDidChangeDirtyState ( ) : Event < DirtyStateChangeEvent > {
return this . _onDidChangeDirtyState . event ;
}
private _onDidChangeLineDirtyState = new EventEmitter < LineDirtyStateChangeEvent > ( ) ;
get onDidChangeLineDirtyState ( ) : Event < LineDirtyStateChangeEvent > {
return this . _onDidChangeLineDirtyState . event ;
}
private readonly _context : Context = { state : { dirty : false } } ;
private readonly _disposable : Disposable ;
private _listenersDisposable : Disposable | undefined ;
private _onDirtyStateChangedDebounced : ( ( dirty : boolean ) = > void ) & IDeferred ;
private _fireDirtyStateChangedDebounced : ( ( ) = > void ) & IDeferred ;
private _checkLineDirtyStateChangedDebounced : ( ( ) = > void ) & IDeferred ;
private _fireLineDirtyStateChangedDebounced : ( ( ) = > void ) & IDeferred ;
private _insiders = false ;
constructor (
private readonly git : GitService
) {
super ( ( ) = > this . dispose ( ) ) ;
this . _onDirtyStateChangedDebounced = Functions . debounce ( this . onDirtyStateChanged , 250 ) ;
this . _fireDirtyStateChangedDebounced = Functions . debounce ( this . fireDirtyStateChanged , 1000 ) ;
this . _checkLineDirtyStateChangedDebounced = Functions . debounce ( this . checkLineDirtyStateChanged , 1000 ) ;
this . _fireLineDirtyStateChangedDebounced = Functions . debounce ( this . fireLineDirtyStateChanged , 1000 ) ;
this . _disposable = Disposable . from (
workspace . onDidChangeConfiguration ( this . onConfigurationChanged , this )
@ -65,29 +98,54 @@ export class GitContextTracker extends Disposable {
this . _disposable && this . _disposable . dispose ( ) ;
}
private onConfigurationChanged ( e : ConfigurationChangeEvent ) {
if ( ! configuration . initializing ( e ) && ! e . affectsConfiguration ( 'git.enabled' , null ! ) ) return ;
private _lineTrackingEnabled : boolean = false ;
setLineTracking ( editor : TextEditor | undefined , enabled : boolean ) {
if ( this . _context . editor !== editor ) return ;
const enabled = workspace . getConfiguration ( 'git' , null ! ) . get < boolean > ( 'enabled' , true ) ;
if ( this . _listenersDisposable !== undefin ed ) {
this . _listenersDisposable . dispose ( ) ;
this . _listenersDisposable = undefined ;
// If we are changing line tracking, reset the current line info, so we will refresh
if ( this . _lineTrackingEnabled !== enabl ed ) {
this . _context . state . line = undefined ;
this . _context . state . lineDirty = undefined ;
}
this . _lineTrackingEnabled = enabled ;
}
setCommandContext ( CommandContext . Enabled , enabled ) ;
private onConfigurationChanged ( e : ConfigurationChangeEvent ) {
const initializing = configuration . initializing ( e ) ;
if ( enabled ) {
this . _listenersDisposable = Disposable . from (
window . onDidChangeActiveTextEditor ( Functions . debounce ( this . onActiveTextEditorChanged , 50 ) , this ) ,
workspace . onDidChangeTextDocument ( this . onTextDocumentChanged , this ) ,
this . git . onDidBlameFail ( this . onBlameFailed , this ) ,
this . git . onDidChange ( this . onGitChanged , this )
) ;
const section = configuration . name ( 'insiders' ) . value ;
if ( initializing || configuration . changed ( e , section ) ) {
this . _insiders = configuration . get < boolean > ( section ) ;
this . updateContext ( BlameabilityChangeReason . EditorChanged , window . activeTextEditor , true ) ;
if ( ! initializing ) {
this . updateContext ( BlameabilityChangeReason . EditorChanged , window . activeTextEditor , true ) ;
}
}
else {
this . updateContext ( BlameabilityChangeReason . EditorChanged , window . activeTextEditor , false ) ;
if ( initializing || e . affectsConfiguration ( 'git.enabled' , null ! ) ) {
const enabled = workspace . getConfiguration ( 'git' , null ! ) . get < boolean > ( 'enabled' , true ) ;
if ( this . _listenersDisposable !== undefined ) {
this . _listenersDisposable . dispose ( ) ;
this . _listenersDisposable = undefined ;
}
setCommandContext ( CommandContext . Enabled , enabled ) ;
if ( enabled ) {
this . _listenersDisposable = Disposable . from (
window . onDidChangeActiveTextEditor ( Functions . debounce ( this . onActiveTextEditorChanged , 50 ) , this ) ,
workspace . onDidChangeTextDocument ( this . onTextDocumentChanged , this ) ,
window . onDidChangeTextEditorSelection ( this . onTextEditorSelectionChanged , this ) ,
this . git . onDidBlameFail ( this . onBlameFailed , this ) ,
this . git . onDidChange ( this . onGitChanged , this )
) ;
this . updateContext ( BlameabilityChangeReason . EditorChanged , window . activeTextEditor , true ) ;
}
else {
this . updateContext ( BlameabilityChangeReason . EditorChanged , window . activeTextEditor , false ) ;
}
}
}
@ -97,6 +155,10 @@ export class GitContextTracker extends Disposable {
// Logger.log('GitContextTracker.onActiveTextEditorChanged', editor && editor.document.uri.fsPath);
// Reset the current line info, so we will refresh
this . _context . state . line = undefined ;
this . _context . state . lineDirty = undefined ;
this . updateContext ( BlameabilityChangeReason . EditorChanged , editor , true ) ;
}
@ -106,11 +168,6 @@ export class GitContextTracker extends Disposable {
this . updateBlameability ( BlameabilityChangeReason . BlameFailed , false ) ;
}
private onDirtyStateChanged ( dirty : boolean ) {
this . _context . state . dirty = dirty ;
this . updateBlameability ( BlameabilityChangeReason . DocumentChanged ) ;
}
private onGitChanged ( e : GitChangeEvent ) {
if ( e . reason !== GitChangeReason . Repositories ) return ;
@ -126,28 +183,95 @@ export class GitContextTracker extends Disposable {
if ( this . _context . editor === undefined || ! TextDocumentComparer . equals ( this . _context . editor . document , e . document ) ) return ;
const dirty = e . document . isDirty ;
const line = ( this . _context . editor && this . _context . editor . selection . active . line ) || - 1 ;
let changed = false ;
if ( this . _context . state . dirty !== dirty || this . _context . state . line !== line ) {
changed = true ;
this . _context . state . dirty = dirty ;
if ( this . _context . state . line !== line ) {
this . _context . state . lineDirty = undefined ;
}
this . _context . state . line = line ;
if ( dirty ) {
this . _fireDirtyStateChangedDebounced . cancel ( ) ;
setImmediate ( ( ) = > this . fireDirtyStateChanged ( ) ) ;
}
else {
this . _fireDirtyStateChangedDebounced ( ) ;
}
}
// If we haven't changed state, kick out
if ( dirty === this . _context . state . dirty ) {
this . _onDirtyStateChangedDebounced . cancel ( ) ;
if ( ! this . _lineTrackingEnabled || ! this . _insiders ) return ;
// If the file dirty state hasn't changed, check if the line has
if ( ! changed ) {
this . _checkLineDirtyStateChangedDebounced ( ) ;
return ;
}
// Logger.log('GitContextTracker.onTextDocumentChanged', `Dirty(${dirty}) state changed`);
this . _context . state . lineDirty = dirty ;
if ( dirty ) {
this . _onDirtyStateChangedDebounced . cancel ( ) ;
this . onDirtyStateChanged ( dirty ) ;
this . _fireLineDirtyStateChangedDebounced . cancel ( ) ;
setImmediate ( ( ) = > this . fireLineDirtyStateChanged ( ) ) ;
}
else {
this . _fireLineDirtyStateChangedDebounced ( ) ;
}
}
return ;
private async checkLineDirtyStateChanged() {
const line = this . _context . state . line ;
if ( this . _context . editor === undefined || line === undefined || line < 0 ) return ;
// Since we only care about this one line, just pass empty lines to align the contents for blaming (and also skip using the cache)
const contents = ` ${ ' \n' . repeat ( line ) } ${ this . _context . editor . document . getText ( new Range ( line , 0 , line , RangeEndOfLineIndex ) ) } \ n ` ;
const blameLine = await this . git . getBlameForLineContents ( this . _context . uri ! , line , contents , { skipCache : true } ) ;
const lineDirty = blameLine !== undefined && blameLine . commit . isUncommitted ;
if ( this . _context . state . lineDirty !== lineDirty ) {
this . _context . state . lineDirty = lineDirty ;
this . _fireLineDirtyStateChangedDebounced . cancel ( ) ;
setImmediate ( ( ) = > this . fireLineDirtyStateChanged ( ) ) ;
}
}
this . _onDirtyStateChangedDebounced ( dirty ) ;
private fireDirtyStateChanged() {
if ( this . _insiders ) {
this . _onDidChangeDirtyState . fire ( {
editor : this._context.editor ,
dirty : this._context.state.dirty
} as DirtyStateChangeEvent ) ;
}
else {
this . updateBlameability ( BlameabilityChangeReason . DocumentChanged ) ;
}
}
private fireLineDirtyStateChanged() {
this . _onDidChangeLineDirtyState . fire ( {
editor : this._context.editor ,
dirty : this._context.state.dirty ,
line : this._context.state.line ,
lineDirty : this._context.state.lineDirty
} as LineDirtyStateChangeEvent ) ;
}
private onTextEditorSelectionChanged ( e : TextEditorSelectionChangeEvent ) {
if ( this . _context . state . line === e . selections [ 0 ] . active . line ) return ;
this . _context . state . line = undefined ;
this . _context . state . lineDirty = false ;
}
private async updateContext ( reason : BlameabilityChangeReason , editor : TextEditor | undefined , force : boolean = false ) {
try {
let dirty = false ;
let revision = false ;
let tracked = false ;
if ( force || this . _context . editor !== editor ) {
@ -167,13 +291,12 @@ export class GitContextTracker extends Disposable {
this . _context . repoDisposable = repo . onDidChange ( this . onRepoChanged , this ) ;
}
this . _context . state . dirty = editor . document . isDirty ;
dirty = editor . document . isDirty ;
revision = ! ! this . _context . uri . sha ;
tracked = await this . git . isTracked ( this . _context . uri ) ;
}
else {
this . _context . uri = undefined ;
this . _context . state . dirty = false ;
this . _context . state . blameable = false ;
}
}
@ -193,6 +316,13 @@ export class GitContextTracker extends Disposable {
setCommandContext ( CommandContext . ActiveFileIsTracked , tracked ) ;
}
if ( this . _context . state . dirty !== dirty ) {
this . _context . state . dirty = dirty ;
if ( this . _insiders ) {
this . _fireDirtyStateChangedDebounced ( ) ;
}
}
this . updateBlameability ( reason , undefined , force ) ;
this . updateRemotes ( ) ;
}
@ -204,7 +334,9 @@ export class GitContextTracker extends Disposable {
private updateBlameability ( reason : BlameabilityChangeReason , blameable? : boolean , force : boolean = false ) {
try {
if ( blameable === undefined ) {
blameable = this . _context . state . tracked && ! this . _context . state . dirty ;
blameable = this . _insiders
? this . _context . state . tracked
: this . _context . state . tracked && ! this . _context . state . dirty ;
}
if ( ! force && this . _context . state . blameable === blameable ) return ;
@ -213,10 +345,11 @@ export class GitContextTracker extends Disposable {
setCommandContext ( CommandContext . ActiveIsBlameable , blameable ) ;
this . _onDidChangeBlameability . fire ( {
editor : this._context.editor ,
blameable : blameable ! ,
editor : this._context && this . _context . editor ,
dirty : this._context.state.dirty ,
reason : reason
} ) ;
} as BlameabilityChangeEvent ) ;
}
catch ( ex ) {
Logger . error ( ex , 'GitContextTracker.updateBlameability' ) ;