3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
- import { DecorationOptions , l10n , Position , Range , TextEditor , TextEditorChange , TextEditorDecorationType , TextEditorChangeKind , ThemeColor , Uri , window , workspace , EventEmitter , ConfigurationChangeEvent , StatusBarItem , StatusBarAlignment , Command , MarkdownString } from 'vscode' ;
6
+ import { DecorationOptions , l10n , Position , Range , TextEditor , TextEditorChange , TextEditorDecorationType , TextEditorChangeKind , ThemeColor , Uri , window , workspace , EventEmitter , ConfigurationChangeEvent , StatusBarItem , StatusBarAlignment , Command , MarkdownString , languages , HoverProvider , CancellationToken , Hover , TextDocument } from 'vscode' ;
7
7
import { Model } from './model' ;
8
8
import { dispose , fromNow , IDisposable } from './util' ;
9
9
import { Repository } from './repository' ;
10
10
import { throttle } from './decorators' ;
11
- import { BlameInformation } from './git' ;
11
+ import { BlameInformation , Commit } from './git' ;
12
12
import { fromGitUri , isGitUri } from './uri' ;
13
13
import { emojify , ensureEmojis } from './emoji' ;
14
14
import { getWorkingTreeAndIndexDiffInformation , getWorkingTreeDiffInformation } from './staging' ;
@@ -55,6 +55,15 @@ function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes:
55
55
return lineNumber ;
56
56
}
57
57
58
+ function getEditorDecorationRange ( lineNumber : number ) : Range {
59
+ const position = new Position ( lineNumber , Number . MAX_SAFE_INTEGER ) ;
60
+ return new Range ( position , position ) ;
61
+ }
62
+
63
+ function isBlameInformation ( object : any ) : object is BlameInformation {
64
+ return Array . isArray ( ( object as BlameInformation ) . ranges ) ;
65
+ }
66
+
58
67
type BlameInformationTemplateTokens = {
59
68
readonly hash : string ;
60
69
readonly hashShort : string ;
@@ -191,32 +200,63 @@ export class GitBlameController {
191
200
} ) ;
192
201
}
193
202
194
- getBlameInformationHover ( documentUri : Uri , blameInformation : BlameInformation | string ) : MarkdownString {
195
- if ( typeof blameInformation === 'string' ) {
196
- return new MarkdownString ( blameInformation , true ) ;
203
+ async getBlameInformationDetailedHover ( documentUri : Uri , blameInformation : BlameInformation ) : Promise < MarkdownString | undefined > {
204
+ const repository = this . _model . getRepository ( documentUri ) ;
205
+ if ( ! repository ) {
206
+ return this . getBlameInformationHover ( documentUri , blameInformation ) ;
207
+ }
208
+
209
+ try {
210
+ const commit = await repository . getCommit ( blameInformation . hash ) ;
211
+ return this . getBlameInformationHover ( documentUri , commit ) ;
212
+ } catch {
213
+ return this . getBlameInformationHover ( documentUri , blameInformation ) ;
197
214
}
215
+ }
198
216
217
+ getBlameInformationHover ( documentUri : Uri , blameInformationOrCommit : BlameInformation | Commit ) : MarkdownString {
199
218
const markdownString = new MarkdownString ( ) ;
200
- markdownString . supportThemeIcons = true ;
201
219
markdownString . isTrusted = true ;
220
+ markdownString . supportHtml = true ;
221
+ markdownString . supportThemeIcons = true ;
202
222
203
- if ( blameInformation . authorName ) {
204
- markdownString . appendMarkdown ( `$(account) **${ blameInformation . authorName } **` ) ;
223
+ if ( blameInformationOrCommit . authorName ) {
224
+ markdownString . appendMarkdown ( `$(account) **${ blameInformationOrCommit . authorName } **` ) ;
205
225
206
- if ( blameInformation . authorDate ) {
207
- const dateString = new Date ( blameInformation . authorDate ) . toLocaleString ( undefined , { year : 'numeric' , month : 'long' , day : 'numeric' , hour : 'numeric' , minute : 'numeric' } ) ;
208
- markdownString . appendMarkdown ( `, $(history) ${ fromNow ( blameInformation . authorDate , true , true ) } (${ dateString } )` ) ;
226
+ if ( blameInformationOrCommit . authorDate ) {
227
+ const dateString = new Date ( blameInformationOrCommit . authorDate ) . toLocaleString ( undefined , { year : 'numeric' , month : 'long' , day : 'numeric' , hour : 'numeric' , minute : 'numeric' } ) ;
228
+ markdownString . appendMarkdown ( `, $(history) ${ fromNow ( blameInformationOrCommit . authorDate , true , true ) } (${ dateString } )` ) ;
209
229
}
210
230
211
231
markdownString . appendMarkdown ( '\n\n' ) ;
212
232
}
213
233
214
- markdownString . appendMarkdown ( `${ emojify ( blameInformation . subject ?? '' ) } \n\n` ) ;
234
+ markdownString . appendMarkdown ( `${ emojify ( isBlameInformation ( blameInformationOrCommit ) ? blameInformationOrCommit . subject ?? '' : blameInformationOrCommit . message ) } \n\n` ) ;
215
235
markdownString . appendMarkdown ( `---\n\n` ) ;
216
236
217
- markdownString . appendMarkdown ( `[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${ encodeURIComponent ( JSON . stringify ( [ documentUri , blameInformation . hash ] ) ) } "${ l10n . t ( 'View Commit' ) } ")` ) ;
237
+ if ( ! isBlameInformation ( blameInformationOrCommit ) && blameInformationOrCommit . shortStat ) {
238
+ markdownString . appendMarkdown ( `<span>${ blameInformationOrCommit . shortStat . files === 1 ?
239
+ l10n . t ( '{0} file changed' , blameInformationOrCommit . shortStat . files ) :
240
+ l10n . t ( '{0} files changed' , blameInformationOrCommit . shortStat . files ) } </span>`) ;
241
+
242
+ if ( blameInformationOrCommit . shortStat . insertions ) {
243
+ markdownString . appendMarkdown ( `, <span style="color:var(--vscode-scmGraph-historyItemHoverAdditionsForeground);">${ blameInformationOrCommit . shortStat . insertions === 1 ?
244
+ l10n . t ( '{0} insertion{1}' , blameInformationOrCommit . shortStat . insertions , '(+)' ) :
245
+ l10n . t ( '{0} insertions{1}' , blameInformationOrCommit . shortStat . insertions , '(+)' ) } </span>`) ;
246
+ }
247
+
248
+ if ( blameInformationOrCommit . shortStat . deletions ) {
249
+ markdownString . appendMarkdown ( `, <span style="color:var(--vscode-scmGraph-historyItemHoverDeletionsForeground);">${ blameInformationOrCommit . shortStat . deletions === 1 ?
250
+ l10n . t ( '{0} deletion{1}' , blameInformationOrCommit . shortStat . deletions , '(-)' ) :
251
+ l10n . t ( '{0} deletions{1}' , blameInformationOrCommit . shortStat . deletions , '(-)' ) } </span>`) ;
252
+ }
253
+
254
+ markdownString . appendMarkdown ( `\n\n---\n\n` ) ;
255
+ }
256
+
257
+ markdownString . appendMarkdown ( `[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${ encodeURIComponent ( JSON . stringify ( [ documentUri , blameInformationOrCommit . hash ] ) ) } "${ l10n . t ( 'View Commit' ) } ")` ) ;
218
258
markdownString . appendMarkdown ( ' ' ) ;
219
- markdownString . appendMarkdown ( `[$(copy) ${ blameInformation . hash . substring ( 0 , 8 ) } ](command:git.blameStatusBarItem.copyContent?${ encodeURIComponent ( JSON . stringify ( blameInformation . hash ) ) } "${ l10n . t ( 'Copy Commit Hash' ) } ")` ) ;
259
+ markdownString . appendMarkdown ( `[$(copy) ${ blameInformationOrCommit . hash . substring ( 0 , 8 ) } ](command:git.blameStatusBarItem.copyContent?${ encodeURIComponent ( JSON . stringify ( blameInformationOrCommit . hash ) ) } "${ l10n . t ( 'Copy Commit Hash' ) } ")` ) ;
220
260
221
261
return markdownString ;
222
262
}
@@ -411,36 +451,81 @@ export class GitBlameController {
411
451
}
412
452
}
413
453
414
- class GitBlameEditorDecoration {
415
- private readonly _decorationType : TextEditorDecorationType ;
454
+ class GitBlameEditorDecoration implements HoverProvider {
455
+ private _decoration : TextEditorDecorationType | undefined ;
456
+ private get decoration ( ) : TextEditorDecorationType {
457
+ if ( ! this . _decoration ) {
458
+ this . _decoration = window . createTextEditorDecorationType ( {
459
+ after : {
460
+ color : new ThemeColor ( 'git.blame.editorDecorationForeground' )
461
+ }
462
+ } ) ;
463
+ }
464
+
465
+ return this . _decoration ;
466
+ }
467
+
468
+ private _hoverDisposable : IDisposable | undefined ;
416
469
private _disposables : IDisposable [ ] = [ ] ;
417
470
418
471
constructor ( private readonly _controller : GitBlameController ) {
419
- this . _decorationType = window . createTextEditorDecorationType ( {
420
- after : {
421
- color : new ThemeColor ( 'git.blame.editorDecorationForeground' )
422
- }
423
- } ) ;
424
- this . _disposables . push ( this . _decorationType ) ;
425
-
426
472
workspace . onDidChangeConfiguration ( this . _onDidChangeConfiguration , this , this . _disposables ) ;
427
473
window . onDidChangeActiveTextEditor ( this . _onDidChangeActiveTextEditor , this , this . _disposables ) ;
428
-
429
474
this . _controller . onDidChangeBlameInformation ( e => this . _updateDecorations ( e ) , this , this . _disposables ) ;
475
+
476
+ this . _onDidChangeConfiguration ( ) ;
430
477
}
431
478
432
- private _onDidChangeConfiguration ( e : ConfigurationChangeEvent ) : void {
433
- if ( ! e . affectsConfiguration ( 'git.blame.editorDecoration.enabled' ) &&
479
+ async provideHover ( document : TextDocument , position : Position , token : CancellationToken ) : Promise < Hover | undefined > {
480
+ if ( token . isCancellationRequested ) {
481
+ return undefined ;
482
+ }
483
+
484
+ const textEditor = window . activeTextEditor ;
485
+ if ( ! textEditor ) {
486
+ return undefined ;
487
+ }
488
+
489
+ // Position must be at the end of the line
490
+ if ( position . character !== document . lineAt ( position . line ) . range . end . character ) {
491
+ return undefined ;
492
+ }
493
+
494
+ // Get blame information
495
+ const blameInformation = this . _controller . textEditorBlameInformation
496
+ . get ( textEditor ) ?. find ( blame => blame . lineNumber === position . line ) ;
497
+
498
+ if ( ! blameInformation || typeof blameInformation . blameInformation === 'string' ) {
499
+ return undefined ;
500
+ }
501
+
502
+ const contents = await this . _controller . getBlameInformationDetailedHover ( textEditor . document . uri , blameInformation . blameInformation ) ;
503
+
504
+ if ( ! contents || token . isCancellationRequested ) {
505
+ return undefined ;
506
+ }
507
+
508
+ return { range : getEditorDecorationRange ( position . line ) , contents : [ contents ] } ;
509
+ }
510
+
511
+ private _onDidChangeConfiguration ( e ?: ConfigurationChangeEvent ) : void {
512
+ if ( e &&
513
+ ! e . affectsConfiguration ( 'git.blame.editorDecoration.enabled' ) &&
434
514
! e . affectsConfiguration ( 'git.blame.editorDecoration.template' ) ) {
435
515
return ;
436
516
}
437
517
438
- for ( const textEditor of window . visibleTextEditors ) {
439
- if ( this . _getConfiguration ( ) . enabled ) {
440
- this . _updateDecorations ( textEditor ) ;
441
- } else {
442
- textEditor . setDecorations ( this . _decorationType , [ ] ) ;
518
+ if ( this . _getConfiguration ( ) . enabled ) {
519
+ if ( window . activeTextEditor ) {
520
+ this . _registerHoverProvider ( ) ;
521
+ this . _updateDecorations ( window . activeTextEditor ) ;
443
522
}
523
+ } else {
524
+ this . _decoration ?. dispose ( ) ;
525
+ this . _decoration = undefined ;
526
+
527
+ this . _hoverDisposable ?. dispose ( ) ;
528
+ this . _hoverDisposable = undefined ;
444
529
}
445
530
}
446
531
@@ -449,11 +534,15 @@ class GitBlameEditorDecoration {
449
534
return ;
450
535
}
451
536
537
+ // Clear decorations
452
538
for ( const editor of window . visibleTextEditors ) {
453
539
if ( editor !== window . activeTextEditor ) {
454
- editor . setDecorations ( this . _decorationType , [ ] ) ;
540
+ editor . setDecorations ( this . decoration , [ ] ) ;
455
541
}
456
542
}
543
+
544
+ // Register hover provider
545
+ this . _registerHoverProvider ( ) ;
457
546
}
458
547
459
548
private _getConfiguration ( ) : { enabled : boolean ; template : string } {
@@ -472,14 +561,14 @@ class GitBlameEditorDecoration {
472
561
473
562
// Only support resources with `file` and `git` schemes
474
563
if ( textEditor . document . uri . scheme !== 'file' && ! isGitUri ( textEditor . document . uri ) ) {
475
- textEditor . setDecorations ( this . _decorationType , [ ] ) ;
564
+ textEditor . setDecorations ( this . decoration , [ ] ) ;
476
565
return ;
477
566
}
478
567
479
568
// Get blame information
480
569
const blameInformation = this . _controller . textEditorBlameInformation . get ( textEditor ) ;
481
570
if ( ! blameInformation ) {
482
- textEditor . setDecorations ( this . _decorationType , [ ] ) ;
571
+ textEditor . setDecorations ( this . decoration , [ ] ) ;
483
572
return ;
484
573
}
485
574
@@ -488,32 +577,43 @@ class GitBlameEditorDecoration {
488
577
const contentText = typeof blame . blameInformation !== 'string'
489
578
? this . _controller . formatBlameInformationMessage ( template , blame . blameInformation )
490
579
: blame . blameInformation ;
491
- const hoverMessage = typeof blame . blameInformation !== 'string'
492
- ? this . _controller . getBlameInformationHover ( textEditor . document . uri , blame . blameInformation )
493
- : undefined ;
494
580
495
- return this . _createDecoration ( blame . lineNumber , contentText , hoverMessage ) ;
581
+ return this . _createDecoration ( blame . lineNumber , contentText ) ;
496
582
} ) ;
497
583
498
- textEditor . setDecorations ( this . _decorationType , decorations ) ;
584
+ textEditor . setDecorations ( this . decoration , decorations ) ;
499
585
}
500
586
501
- private _createDecoration ( lineNumber : number , contentText : string , hoverMessage : MarkdownString | undefined ) : DecorationOptions {
502
- const position = new Position ( lineNumber , Number . MAX_SAFE_INTEGER ) ;
503
-
587
+ private _createDecoration ( lineNumber : number , contentText : string ) : DecorationOptions {
504
588
return {
505
- hoverMessage,
506
- range : new Range ( position , position ) ,
589
+ range : getEditorDecorationRange ( lineNumber ) ,
507
590
renderOptions : {
508
591
after : {
509
- contentText : ` ${ contentText } ` ,
592
+ contentText,
510
593
margin : '0 0 0 50px'
511
594
}
512
595
} ,
513
596
} ;
514
597
}
515
598
599
+ private _registerHoverProvider ( ) : void {
600
+ this . _hoverDisposable ?. dispose ( ) ;
601
+
602
+ if ( window . activeTextEditor ?. document . uri . scheme === 'file' ||
603
+ window . activeTextEditor ?. document . uri . scheme === 'git' ) {
604
+ this . _hoverDisposable = languages . registerHoverProvider ( {
605
+ pattern : window . activeTextEditor . document . uri . fsPath
606
+ } , this ) ;
607
+ }
608
+ }
609
+
516
610
dispose ( ) {
611
+ this . _decoration ?. dispose ( ) ;
612
+ this . _decoration = undefined ;
613
+
614
+ this . _hoverDisposable ?. dispose ( ) ;
615
+ this . _hoverDisposable = undefined ;
616
+
517
617
this . _disposables = dispose ( this . _disposables ) ;
518
618
}
519
619
}
0 commit comments