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