1+ 'use strict' ;
2+ import { Functions , Objects } from '../system' ;
3+ import { DecorationRenderOptions , Disposable , Event , EventEmitter , ExtensionContext , OverviewRulerLane , TextDocument , TextDocumentChangeEvent , TextEditor , TextEditorDecorationType , TextEditorViewColumnChangeEvent , window , workspace } from 'vscode' ;
4+ import { AnnotationProviderBase } from './annotationProvider' ;
5+ import { TextDocumentComparer , TextEditorComparer } from '../comparers' ;
6+ import { BlameLineHighlightLocations , ExtensionKey , FileAnnotationType , IConfig , themeDefaults } from '../configuration' ;
7+ import { BlameabilityChangeEvent , GitContextTracker , GitService , GitUri } from '../gitService' ;
8+ import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider' ;
9+ import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider' ;
10+ import { Logger } from '../logger' ;
11+ import { WhitespaceController } from './whitespaceController' ;
12+
13+ export const Decorations = {
14+ annotation : window . createTextEditorDecorationType ( {
15+ isWholeLine : true
16+ } as DecorationRenderOptions ) ,
17+ highlight : undefined as TextEditorDecorationType | undefined
18+ } ;
19+
20+ export class AnnotationController extends Disposable {
21+
22+ private _onDidToggleAnnotations = new EventEmitter < void > ( ) ;
23+ get onDidToggleAnnotations ( ) : Event < void > {
24+ return this . _onDidToggleAnnotations . event ;
25+ }
26+
27+ private _annotationsDisposable : Disposable | undefined ;
28+ private _annotationProviders : Map < number , AnnotationProviderBase > = new Map ( ) ;
29+ private _config : IConfig ;
30+ private _disposable : Disposable ;
31+ private _whitespaceController : WhitespaceController | undefined ;
32+
33+ constructor ( private context : ExtensionContext , private git : GitService , private gitContextTracker : GitContextTracker ) {
34+ super ( ( ) => this . dispose ( ) ) ;
35+
36+ this . _onConfigurationChanged ( ) ;
37+
38+ const subscriptions : Disposable [ ] = [ ] ;
39+
40+ subscriptions . push ( workspace . onDidChangeConfiguration ( this . _onConfigurationChanged , this ) ) ;
41+
42+ this . _disposable = Disposable . from ( ...subscriptions ) ;
43+ }
44+
45+ dispose ( ) {
46+ this . _annotationProviders . forEach ( async ( p , i ) => await this . clear ( i ) ) ;
47+
48+ Decorations . annotation && Decorations . annotation . dispose ( ) ;
49+ Decorations . highlight && Decorations . highlight . dispose ( ) ;
50+
51+ this . _annotationsDisposable && this . _annotationsDisposable . dispose ( ) ;
52+ this . _whitespaceController && this . _whitespaceController . dispose ( ) ;
53+ this . _disposable && this . _disposable . dispose ( ) ;
54+ }
55+
56+ private _onConfigurationChanged ( ) {
57+ let toggleWhitespace = workspace . getConfiguration ( `${ ExtensionKey } .advanced.toggleWhitespace` ) . get < boolean > ( 'enabled' ) ;
58+ if ( ! toggleWhitespace ) {
59+ // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
60+ // TODO: detect monospace font
61+ toggleWhitespace = workspace . getConfiguration ( 'editor' ) . get < boolean > ( 'fontLigatures' ) ;
62+ }
63+
64+ if ( toggleWhitespace && ! this . _whitespaceController ) {
65+ this . _whitespaceController = new WhitespaceController ( ) ;
66+ }
67+ else if ( ! toggleWhitespace && this . _whitespaceController ) {
68+ this . _whitespaceController . dispose ( ) ;
69+ this . _whitespaceController = undefined ;
70+ }
71+
72+ const cfg = workspace . getConfiguration ( ) . get < IConfig > ( ExtensionKey ) ! ;
73+ const cfgHighlight = cfg . blame . file . lineHighlight ;
74+ const cfgTheme = cfg . theme . lineHighlight ;
75+
76+ let changed = false ;
77+
78+ if ( ! Objects . areEquivalent ( cfgHighlight , this . _config && this . _config . blame . file . lineHighlight ) ||
79+ ! Objects . areEquivalent ( cfgTheme , this . _config && this . _config . theme . lineHighlight ) ) {
80+ changed = true ;
81+
82+ Decorations . highlight && Decorations . highlight . dispose ( ) ;
83+
84+ if ( cfgHighlight . enabled ) {
85+ Decorations . highlight = window . createTextEditorDecorationType ( {
86+ gutterIconSize : 'contain' ,
87+ isWholeLine : true ,
88+ overviewRulerLane : OverviewRulerLane . Right ,
89+ dark : {
90+ backgroundColor : cfgHighlight . locations . includes ( BlameLineHighlightLocations . Line )
91+ ? cfgTheme . dark . backgroundColor || themeDefaults . lineHighlight . dark . backgroundColor
92+ : undefined ,
93+ gutterIconPath : cfgHighlight . locations . includes ( BlameLineHighlightLocations . Gutter )
94+ ? this . context . asAbsolutePath ( 'images/blame-dark.svg' )
95+ : undefined ,
96+ overviewRulerColor : cfgHighlight . locations . includes ( BlameLineHighlightLocations . OverviewRuler )
97+ ? cfgTheme . dark . overviewRulerColor || themeDefaults . lineHighlight . dark . overviewRulerColor
98+ : undefined
99+ } ,
100+ light : {
101+ backgroundColor : cfgHighlight . locations . includes ( BlameLineHighlightLocations . Line )
102+ ? cfgTheme . light . backgroundColor || themeDefaults . lineHighlight . light . backgroundColor
103+ : undefined ,
104+ gutterIconPath : cfgHighlight . locations . includes ( BlameLineHighlightLocations . Gutter )
105+ ? this . context . asAbsolutePath ( 'images/blame-light.svg' )
106+ : undefined ,
107+ overviewRulerColor : cfgHighlight . locations . includes ( BlameLineHighlightLocations . OverviewRuler )
108+ ? cfgTheme . light . overviewRulerColor || themeDefaults . lineHighlight . light . overviewRulerColor
109+ : undefined
110+ }
111+ } ) ;
112+ }
113+ else {
114+ Decorations . highlight = undefined ;
115+ }
116+ }
117+
118+ if ( ! Objects . areEquivalent ( cfg . blame . file , this . _config && this . _config . blame . file ) ||
119+ ! Objects . areEquivalent ( cfg . annotations , this . _config && this . _config . annotations ) ||
120+ ! Objects . areEquivalent ( cfg . theme . annotations , this . _config && this . _config . theme . annotations ) ) {
121+ changed = true ;
122+ }
123+
124+ this . _config = cfg ;
125+
126+ if ( changed ) {
127+ // Since the configuration has changed -- reset any visible annotations
128+ for ( const provider of this . _annotationProviders . values ( ) ) {
129+ if ( provider === undefined ) continue ;
130+
131+ provider . reset ( ) ;
132+ }
133+ }
134+ }
135+
136+ async clear ( column : number ) {
137+ const provider = this . _annotationProviders . get ( column ) ;
138+ if ( ! provider ) return ;
139+
140+ this . _annotationProviders . delete ( column ) ;
141+ await provider . dispose ( ) ;
142+
143+ if ( this . _annotationProviders . size === 0 ) {
144+ Logger . log ( `Remove listener registrations for annotations` ) ;
145+ this . _annotationsDisposable && this . _annotationsDisposable . dispose ( ) ;
146+ this . _annotationsDisposable = undefined ;
147+ }
148+
149+ this . _onDidToggleAnnotations . fire ( ) ;
150+ }
151+
152+ getAnnotationType ( editor : TextEditor ) : FileAnnotationType | undefined {
153+ const provider = this . getProvider ( editor ) ;
154+ return provider === undefined ? undefined : provider . annotationType ;
155+ }
156+
157+ getProvider ( editor : TextEditor ) : AnnotationProviderBase | undefined {
158+ if ( ! editor || ! editor . document || ! this . git . isEditorBlameable ( editor ) ) return undefined ;
159+
160+ return this . _annotationProviders . get ( editor . viewColumn || - 1 ) ;
161+ }
162+
163+ async showAnnotations ( editor : TextEditor , type : FileAnnotationType , shaOrLine ?: string | number ) : Promise < boolean > {
164+ if ( ! editor || ! editor . document || ! this . git . isEditorBlameable ( editor ) ) return false ;
165+
166+ const currentProvider = this . _annotationProviders . get ( editor . viewColumn || - 1 ) ;
167+ if ( currentProvider && TextEditorComparer . equals ( currentProvider . editor , editor ) ) {
168+ await currentProvider . selection ( shaOrLine ) ;
169+ return true ;
170+ }
171+
172+ const gitUri = await GitUri . fromUri ( editor . document . uri , this . git ) ;
173+
174+ let provider : AnnotationProviderBase | undefined = undefined ;
175+ switch ( type ) {
176+ case FileAnnotationType . Gutter :
177+ provider = new GutterBlameAnnotationProvider ( this . context , editor , Decorations . annotation , Decorations . highlight , this . _whitespaceController , this . git , gitUri ) ;
178+ break ;
179+ case FileAnnotationType . Hover :
180+ provider = new HoverBlameAnnotationProvider ( this . context , editor , Decorations . annotation , Decorations . highlight , this . _whitespaceController , this . git , gitUri ) ;
181+ break ;
182+ }
183+ if ( provider === undefined || ! ( await provider . validate ( ) ) ) return false ;
184+
185+ if ( currentProvider ) {
186+ await this . clear ( currentProvider . editor . viewColumn || - 1 ) ;
187+ }
188+
189+ if ( ! this . _annotationsDisposable && this . _annotationProviders . size === 0 ) {
190+ Logger . log ( `Add listener registrations for annotations` ) ;
191+
192+ const subscriptions : Disposable [ ] = [ ] ;
193+
194+ subscriptions . push ( window . onDidChangeVisibleTextEditors ( Functions . debounce ( this . _onVisibleTextEditorsChanged , 100 ) , this ) ) ;
195+ subscriptions . push ( window . onDidChangeTextEditorViewColumn ( this . _onTextEditorViewColumnChanged , this ) ) ;
196+ subscriptions . push ( workspace . onDidChangeTextDocument ( this . _onTextDocumentChanged , this ) ) ;
197+ subscriptions . push ( workspace . onDidCloseTextDocument ( this . _onTextDocumentClosed , this ) ) ;
198+ subscriptions . push ( this . gitContextTracker . onDidBlameabilityChange ( this . _onBlameabilityChanged , this ) ) ;
199+
200+ this . _annotationsDisposable = Disposable . from ( ...subscriptions ) ;
201+ }
202+
203+ this . _annotationProviders . set ( editor . viewColumn || - 1 , provider ) ;
204+ if ( await provider . provideAnnotation ( shaOrLine ) ) {
205+ this . _onDidToggleAnnotations . fire ( ) ;
206+ return true ;
207+ }
208+ return false ;
209+ }
210+
211+ async toggleAnnotations ( editor : TextEditor , type : FileAnnotationType , shaOrLine ?: string | number ) : Promise < boolean > {
212+ if ( ! editor || ! editor . document || ! this . git . isEditorBlameable ( editor ) ) return false ;
213+
214+ const provider = this . _annotationProviders . get ( editor . viewColumn || - 1 ) ;
215+ if ( provider === undefined ) return this . showAnnotations ( editor , type , shaOrLine ) ;
216+
217+ await this . clear ( provider . editor . viewColumn || - 1 ) ;
218+ return false ;
219+ }
220+
221+ private _onBlameabilityChanged ( e : BlameabilityChangeEvent ) {
222+ if ( e . blameable || ! e . editor ) return ;
223+
224+ for ( const [ key , p ] of this . _annotationProviders ) {
225+ if ( ! TextDocumentComparer . equals ( p . document , e . editor . document ) ) continue ;
226+
227+ Logger . log ( 'BlameabilityChanged:' , `Clear annotations for column ${ key } ` ) ;
228+ this . clear ( key ) ;
229+ }
230+ }
231+
232+ private _onTextDocumentChanged ( e : TextDocumentChangeEvent ) {
233+ for ( const [ key , p ] of this . _annotationProviders ) {
234+ if ( ! TextDocumentComparer . equals ( p . document , e . document ) ) continue ;
235+
236+ // We have to defer because isDirty is not reliable inside this event
237+ setTimeout ( ( ) => {
238+ // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
239+ if ( e . document . isDirty ) return ;
240+
241+ // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
242+ // Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking
243+ Logger . log ( 'TextDocumentChanged:' , `Clear annotations for column ${ key } ` ) ;
244+ this . clear ( key ) ;
245+ } , 1 ) ;
246+ }
247+ }
248+
249+ private _onTextDocumentClosed ( e : TextDocument ) {
250+ for ( const [ key , p ] of this . _annotationProviders ) {
251+ if ( ! TextDocumentComparer . equals ( p . document , e ) ) continue ;
252+
253+ Logger . log ( 'TextDocumentClosed:' , `Clear annotations for column ${ key } ` ) ;
254+ this . clear ( key ) ;
255+ }
256+ }
257+
258+ private async _onTextEditorViewColumnChanged ( e : TextEditorViewColumnChangeEvent ) {
259+ const viewColumn = e . viewColumn || - 1 ;
260+
261+ Logger . log ( 'TextEditorViewColumnChanged:' , `Clear annotations for column ${ viewColumn } ` ) ;
262+ await this . clear ( viewColumn ) ;
263+
264+ for ( const [ key , p ] of this . _annotationProviders ) {
265+ if ( ! TextEditorComparer . equals ( p . editor , e . textEditor ) ) continue ;
266+
267+ Logger . log ( 'TextEditorViewColumnChanged:' , `Clear annotations for column ${ key } ` ) ;
268+ await this . clear ( key ) ;
269+ }
270+ }
271+
272+ private async _onVisibleTextEditorsChanged ( e : TextEditor [ ] ) {
273+ if ( e . every ( _ => _ . document . uri . scheme === 'inmemory' ) ) return ;
274+
275+ for ( const [ key , p ] of this . _annotationProviders ) {
276+ if ( e . some ( _ => TextEditorComparer . equals ( p . editor , _ ) ) ) continue ;
277+
278+ Logger . log ( 'VisibleTextEditorsChanged:' , `Clear annotations for column ${ key } ` ) ;
279+ this . clear ( key ) ;
280+ }
281+ }
282+ }
0 commit comments