|
|
@ -1,302 +1,302 @@ |
|
|
|
'use strict'; |
|
|
|
import { Functions, Objects } from './system'; |
|
|
|
import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; |
|
|
|
import { BlameAnnotationController } from './blameAnnotationController'; |
|
|
|
import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter'; |
|
|
|
import { TextEditorComparer } from './comparers'; |
|
|
|
import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; |
|
|
|
import { DocumentSchemes, ExtensionKey } from './constants'; |
|
|
|
import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; |
|
|
|
import * as moment from 'moment'; |
|
|
|
|
|
|
|
const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ |
|
|
|
after: { |
|
|
|
margin: '0 0 0 4em' |
|
|
|
} |
|
|
|
} as DecorationRenderOptions); |
|
|
|
|
|
|
|
export class BlameActiveLineController extends Disposable { |
|
|
|
|
|
|
|
private _activeEditorLineDisposable: Disposable | undefined; |
|
|
|
private _blameable: boolean; |
|
|
|
private _config: IConfig; |
|
|
|
private _currentLine: number = -1; |
|
|
|
private _disposable: Disposable; |
|
|
|
private _editor: TextEditor | undefined; |
|
|
|
private _statusBarItem: StatusBarItem | undefined; |
|
|
|
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>; |
|
|
|
private _uri: GitUri; |
|
|
|
|
|
|
|
constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: BlameAnnotationController) { |
|
|
|
super(() => this.dispose()); |
|
|
|
|
|
|
|
this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250); |
|
|
|
|
|
|
|
this._onConfigurationChanged(); |
|
|
|
|
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); |
|
|
|
subscriptions.push(git.onDidChangeGitCache(this._onGitCacheChanged, this)); |
|
|
|
subscriptions.push(annotationController.onDidToggleBlameAnnotations(this._onBlameAnnotationToggled, this)); |
|
|
|
|
|
|
|
this._disposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
|
|
|
|
dispose() { |
|
|
|
this._editor && this._editor.setDecorations(activeLineDecoration, []); |
|
|
|
|
|
|
|
this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose(); |
|
|
|
this._statusBarItem && this._statusBarItem.dispose(); |
|
|
|
this._disposable && this._disposable.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
private _onConfigurationChanged() { |
|
|
|
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; |
|
|
|
|
|
|
|
let changed: boolean = false; |
|
|
|
|
|
|
|
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { |
|
|
|
changed = true; |
|
|
|
if (cfg.statusBar.enabled) { |
|
|
|
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; |
|
|
|
if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) { |
|
|
|
this._statusBarItem.dispose(); |
|
|
|
this._statusBarItem = undefined; |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); |
|
|
|
this._statusBarItem.command = cfg.statusBar.command; |
|
|
|
} |
|
|
|
else if (!cfg.statusBar.enabled && this._statusBarItem) { |
|
|
|
this._statusBarItem.dispose(); |
|
|
|
this._statusBarItem = undefined; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!Objects.areEquivalent(cfg.blame.annotation.activeLine, this._config && this._config.blame.annotation.activeLine)) { |
|
|
|
changed = true; |
|
|
|
if (cfg.blame.annotation.activeLine !== 'off' && this._editor) { |
|
|
|
this._editor.setDecorations(activeLineDecoration, []); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!Objects.areEquivalent(cfg.blame.annotation.activeLineDarkColor, this._config && this._config.blame.annotation.activeLineDarkColor) || |
|
|
|
!Objects.areEquivalent(cfg.blame.annotation.activeLineLightColor, this._config && this._config.blame.annotation.activeLineLightColor)) { |
|
|
|
changed = true; |
|
|
|
} |
|
|
|
|
|
|
|
this._config = cfg; |
|
|
|
|
|
|
|
if (!changed) return; |
|
|
|
|
|
|
|
let trackActiveLine = cfg.statusBar.enabled || cfg.blame.annotation.activeLine !== 'off'; |
|
|
|
if (trackActiveLine && !this._activeEditorLineDisposable) { |
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); |
|
|
|
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); |
|
|
|
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); |
|
|
|
|
|
|
|
this._activeEditorLineDisposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
else if (!trackActiveLine && this._activeEditorLineDisposable) { |
|
|
|
this._activeEditorLineDisposable.dispose(); |
|
|
|
this._activeEditorLineDisposable = undefined; |
|
|
|
} |
|
|
|
|
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private isEditorBlameable(editor: TextEditor | undefined): boolean { |
|
|
|
if (editor === undefined || editor.document === undefined) return false; |
|
|
|
|
|
|
|
if (!this.git.isTrackable(editor.document.uri)) return false; |
|
|
|
if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false; |
|
|
|
|
|
|
|
return this.git.isEditorBlameable(editor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _onActiveTextEditorChanged(editor: TextEditor | undefined) { |
|
|
|
this._currentLine = -1; |
|
|
|
|
|
|
|
const previousEditor = this._editor; |
|
|
|
previousEditor && previousEditor.setDecorations(activeLineDecoration, []); |
|
|
|
|
|
|
|
if (editor === undefined || !this.isEditorBlameable(editor)) { |
|
|
|
this.clear(editor); |
|
|
|
|
|
|
|
this._editor = undefined; |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; |
|
|
|
this._editor = editor; |
|
|
|
this._uri = await GitUri.fromUri(editor.document.uri, this.git); |
|
|
|
|
|
|
|
const maxLines = this._config.advanced.caching.statusBar.maxLines; |
|
|
|
// If caching is on and the file is small enough -- kick off a blame for the whole file
|
|
|
|
if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { |
|
|
|
this.git.getBlameForFile(this._uri); |
|
|
|
} |
|
|
|
|
|
|
|
this._updateBlame(editor.selection.active.line, editor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameabilityChanged(e: BlameabilityChangeEvent) { |
|
|
|
this._blameable = e.blameable; |
|
|
|
if (!e.blameable || !this._editor) { |
|
|
|
this.clear(e.editor); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Make sure this is for the editor we are tracking
|
|
|
|
if (!TextEditorComparer.equals(this._editor, e.editor)) return; |
|
|
|
|
|
|
|
this._updateBlame(this._editor.selection.active.line, this._editor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameAnnotationToggled() { |
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onGitCacheChanged() { |
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<void> { |
|
|
|
// Make sure this is for the editor we are tracking
|
|
|
|
if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; |
|
|
|
|
|
|
|
const line = e.selections[0].active.line; |
|
|
|
if (line === this._currentLine) return; |
|
|
|
this._currentLine = line; |
|
|
|
|
|
|
|
if (!this._uri && e.textEditor) { |
|
|
|
this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git); |
|
|
|
} |
|
|
|
|
|
|
|
this._updateBlameDebounced(line, e.textEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _updateBlame(line: number, editor: TextEditor) { |
|
|
|
line = line - this._uri.offset; |
|
|
|
|
|
|
|
let commit: GitCommit | undefined = undefined; |
|
|
|
let commitLine: IGitCommitLine | undefined = undefined; |
|
|
|
// Since blame information isn't valid when there are unsaved changes -- don't show any status
|
|
|
|
if (this._blameable && line >= 0) { |
|
|
|
const blameLine = await this.git.getBlameForLine(this._uri, line); |
|
|
|
commitLine = blameLine === undefined ? undefined : blameLine.line; |
|
|
|
commit = blameLine === undefined ? undefined : blameLine.commit; |
|
|
|
} |
|
|
|
|
|
|
|
if (commit !== undefined && commitLine !== undefined) { |
|
|
|
this.show(commit, commitLine, editor); |
|
|
|
} |
|
|
|
else { |
|
|
|
this.clear(editor); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
clear(editor: TextEditor | undefined, previousEditor?: TextEditor) { |
|
|
|
editor && editor.setDecorations(activeLineDecoration, []); |
|
|
|
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
|
|
|
|
if (editor) { |
|
|
|
setTimeout(() => editor.setDecorations(activeLineDecoration, []), 1); |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem && this._statusBarItem.hide(); |
|
|
|
} |
|
|
|
|
|
|
|
async show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { |
|
|
|
// I have no idea why I need this protection -- but it happens
|
|
|
|
if (!editor.document) return; |
|
|
|
|
|
|
|
if (this._config.statusBar.enabled && this._statusBarItem !== undefined) { |
|
|
|
switch (this._config.statusBar.date) { |
|
|
|
case 'off': |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}`; |
|
|
|
break; |
|
|
|
case 'absolute': |
|
|
|
const dateFormat = this._config.statusBar.dateFormat || 'MMMM Do, YYYY h:MMa'; |
|
|
|
let date: string; |
|
|
|
try { |
|
|
|
date = moment(commit.date).format(dateFormat); |
|
|
|
} catch (ex) { |
|
|
|
date = moment(commit.date).format('MMMM Do, YYYY h:MMa'); |
|
|
|
} |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${date}`; |
|
|
|
break; |
|
|
|
default: |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
switch (this._config.statusBar.command) { |
|
|
|
case StatusBarCommand.BlameAnnotate: |
|
|
|
this._statusBarItem.tooltip = 'Toggle Blame Annotations'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowBlameHistory: |
|
|
|
this._statusBarItem.tooltip = 'Open Blame History Explorer'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowFileHistory: |
|
|
|
this._statusBarItem.tooltip = 'Open File History Explorer'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.DiffWithPrevious: |
|
|
|
this._statusBarItem.tooltip = 'Compare with Previous Commit'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ToggleCodeLens: |
|
|
|
this._statusBarItem.tooltip = 'Toggle Git CodeLens'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCommitDetails: |
|
|
|
this._statusBarItem.tooltip = 'Show Commit Details'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCommitFileDetails: |
|
|
|
this._statusBarItem.tooltip = 'Show Line Commit Details'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickFileHistory: |
|
|
|
this._statusBarItem.tooltip = 'Show File History'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCurrentBranchHistory: |
|
|
|
this._statusBarItem.tooltip = 'Show Branch History'; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem.show(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this._config.blame.annotation.activeLine !== 'off') { |
|
|
|
const activeLine = this._config.blame.annotation.activeLine; |
|
|
|
const offset = this._uri.offset; |
|
|
|
|
|
|
|
const cfg = { |
|
|
|
annotation: { |
|
|
|
sha: true, |
|
|
|
author: this._config.statusBar.enabled ? false : this._config.blame.annotation.author, |
|
|
|
date: this._config.statusBar.enabled ? 'off' : this._config.blame.annotation.date, |
|
|
|
message: true |
|
|
|
} |
|
|
|
} as IBlameConfig; |
|
|
|
|
|
|
|
const annotation = BlameAnnotationFormatter.getAnnotation(cfg, commit, BlameAnnotationFormat.Unconstrained); |
|
|
|
|
|
|
|
// Get the full commit message -- since blame only returns the summary
|
|
|
|
let logCommit: GitCommit | undefined = undefined; |
|
|
|
if (!commit.isUncommitted) { |
|
|
|
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha); |
|
|
|
} |
|
|
|
|
|
|
|
// I have no idea why I need this protection -- but it happens
|
|
|
|
if (!editor.document) return; |
|
|
|
|
|
|
|
let hoverMessage: string | string[] | undefined = undefined; |
|
|
|
if (activeLine !== 'inline') { |
|
|
|
// If the messages match (or we couldn't find the log), then this is a possible duplicate annotation
|
|
|
|
const possibleDuplicate = !logCommit || logCommit.message === commit.message; |
|
|
|
// If we don't have a possible dupe or we aren't showing annotations get the hover message
|
|
|
|
if (!commit.isUncommitted && (!possibleDuplicate || !this.annotationController.isAnnotating(editor))) { |
|
|
|
hoverMessage = BlameAnnotationFormatter.getAnnotationHover(cfg, blameLine, logCommit || commit); |
|
|
|
'use strict'; |
|
|
|
import { Functions, Objects } from './system'; |
|
|
|
import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; |
|
|
|
import { BlameAnnotationController } from './blameAnnotationController'; |
|
|
|
import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter'; |
|
|
|
import { TextEditorComparer } from './comparers'; |
|
|
|
import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; |
|
|
|
import { DocumentSchemes, ExtensionKey } from './constants'; |
|
|
|
import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; |
|
|
|
import * as moment from 'moment'; |
|
|
|
|
|
|
|
const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ |
|
|
|
after: { |
|
|
|
margin: '0 0 0 4em' |
|
|
|
} |
|
|
|
} as DecorationRenderOptions); |
|
|
|
|
|
|
|
export class BlameActiveLineController extends Disposable { |
|
|
|
|
|
|
|
private _activeEditorLineDisposable: Disposable | undefined; |
|
|
|
private _blameable: boolean; |
|
|
|
private _config: IConfig; |
|
|
|
private _currentLine: number = -1; |
|
|
|
private _disposable: Disposable; |
|
|
|
private _editor: TextEditor | undefined; |
|
|
|
private _statusBarItem: StatusBarItem | undefined; |
|
|
|
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>; |
|
|
|
private _uri: GitUri; |
|
|
|
|
|
|
|
constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: BlameAnnotationController) { |
|
|
|
super(() => this.dispose()); |
|
|
|
|
|
|
|
this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250); |
|
|
|
|
|
|
|
this._onConfigurationChanged(); |
|
|
|
|
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); |
|
|
|
subscriptions.push(git.onDidChangeGitCache(this._onGitCacheChanged, this)); |
|
|
|
subscriptions.push(annotationController.onDidToggleBlameAnnotations(this._onBlameAnnotationToggled, this)); |
|
|
|
|
|
|
|
this._disposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
|
|
|
|
dispose() { |
|
|
|
this._editor && this._editor.setDecorations(activeLineDecoration, []); |
|
|
|
|
|
|
|
this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose(); |
|
|
|
this._statusBarItem && this._statusBarItem.dispose(); |
|
|
|
this._disposable && this._disposable.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
private _onConfigurationChanged() { |
|
|
|
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; |
|
|
|
|
|
|
|
let changed: boolean = false; |
|
|
|
|
|
|
|
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { |
|
|
|
changed = true; |
|
|
|
if (cfg.statusBar.enabled) { |
|
|
|
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; |
|
|
|
if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) { |
|
|
|
this._statusBarItem.dispose(); |
|
|
|
this._statusBarItem = undefined; |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); |
|
|
|
this._statusBarItem.command = cfg.statusBar.command; |
|
|
|
} |
|
|
|
else if (!cfg.statusBar.enabled && this._statusBarItem) { |
|
|
|
this._statusBarItem.dispose(); |
|
|
|
this._statusBarItem = undefined; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!Objects.areEquivalent(cfg.blame.annotation.activeLine, this._config && this._config.blame.annotation.activeLine)) { |
|
|
|
changed = true; |
|
|
|
if (cfg.blame.annotation.activeLine !== 'off' && this._editor) { |
|
|
|
this._editor.setDecorations(activeLineDecoration, []); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!Objects.areEquivalent(cfg.blame.annotation.activeLineDarkColor, this._config && this._config.blame.annotation.activeLineDarkColor) || |
|
|
|
!Objects.areEquivalent(cfg.blame.annotation.activeLineLightColor, this._config && this._config.blame.annotation.activeLineLightColor)) { |
|
|
|
changed = true; |
|
|
|
} |
|
|
|
|
|
|
|
this._config = cfg; |
|
|
|
|
|
|
|
if (!changed) return; |
|
|
|
|
|
|
|
let trackActiveLine = cfg.statusBar.enabled || cfg.blame.annotation.activeLine !== 'off'; |
|
|
|
if (trackActiveLine && !this._activeEditorLineDisposable) { |
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); |
|
|
|
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); |
|
|
|
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); |
|
|
|
|
|
|
|
this._activeEditorLineDisposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
else if (!trackActiveLine && this._activeEditorLineDisposable) { |
|
|
|
this._activeEditorLineDisposable.dispose(); |
|
|
|
this._activeEditorLineDisposable = undefined; |
|
|
|
} |
|
|
|
|
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private isEditorBlameable(editor: TextEditor | undefined): boolean { |
|
|
|
if (editor === undefined || editor.document === undefined) return false; |
|
|
|
|
|
|
|
if (!this.git.isTrackable(editor.document.uri)) return false; |
|
|
|
if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false; |
|
|
|
|
|
|
|
return this.git.isEditorBlameable(editor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _onActiveTextEditorChanged(editor: TextEditor | undefined) { |
|
|
|
this._currentLine = -1; |
|
|
|
|
|
|
|
const previousEditor = this._editor; |
|
|
|
previousEditor && previousEditor.setDecorations(activeLineDecoration, []); |
|
|
|
|
|
|
|
if (editor === undefined || !this.isEditorBlameable(editor)) { |
|
|
|
this.clear(editor); |
|
|
|
|
|
|
|
this._editor = undefined; |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; |
|
|
|
this._editor = editor; |
|
|
|
this._uri = await GitUri.fromUri(editor.document.uri, this.git); |
|
|
|
|
|
|
|
const maxLines = this._config.advanced.caching.statusBar.maxLines; |
|
|
|
// If caching is on and the file is small enough -- kick off a blame for the whole file
|
|
|
|
if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { |
|
|
|
this.git.getBlameForFile(this._uri); |
|
|
|
} |
|
|
|
|
|
|
|
this._updateBlame(editor.selection.active.line, editor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameabilityChanged(e: BlameabilityChangeEvent) { |
|
|
|
this._blameable = e.blameable; |
|
|
|
if (!e.blameable || !this._editor) { |
|
|
|
this.clear(e.editor); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Make sure this is for the editor we are tracking
|
|
|
|
if (!TextEditorComparer.equals(this._editor, e.editor)) return; |
|
|
|
|
|
|
|
this._updateBlame(this._editor.selection.active.line, this._editor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameAnnotationToggled() { |
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private _onGitCacheChanged() { |
|
|
|
this._onActiveTextEditorChanged(window.activeTextEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<void> { |
|
|
|
// Make sure this is for the editor we are tracking
|
|
|
|
if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; |
|
|
|
|
|
|
|
const line = e.selections[0].active.line; |
|
|
|
if (line === this._currentLine) return; |
|
|
|
this._currentLine = line; |
|
|
|
|
|
|
|
if (!this._uri && e.textEditor) { |
|
|
|
this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git); |
|
|
|
} |
|
|
|
|
|
|
|
this._updateBlameDebounced(line, e.textEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _updateBlame(line: number, editor: TextEditor) { |
|
|
|
line = line - this._uri.offset; |
|
|
|
|
|
|
|
let commit: GitCommit | undefined = undefined; |
|
|
|
let commitLine: IGitCommitLine | undefined = undefined; |
|
|
|
// Since blame information isn't valid when there are unsaved changes -- don't show any status
|
|
|
|
if (this._blameable && line >= 0) { |
|
|
|
const blameLine = await this.git.getBlameForLine(this._uri, line); |
|
|
|
commitLine = blameLine === undefined ? undefined : blameLine.line; |
|
|
|
commit = blameLine === undefined ? undefined : blameLine.commit; |
|
|
|
} |
|
|
|
|
|
|
|
if (commit !== undefined && commitLine !== undefined) { |
|
|
|
this.show(commit, commitLine, editor); |
|
|
|
} |
|
|
|
else { |
|
|
|
this.clear(editor); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
clear(editor: TextEditor | undefined, previousEditor?: TextEditor) { |
|
|
|
editor && editor.setDecorations(activeLineDecoration, []); |
|
|
|
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
|
|
|
|
if (editor) { |
|
|
|
setTimeout(() => editor.setDecorations(activeLineDecoration, []), 1); |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem && this._statusBarItem.hide(); |
|
|
|
} |
|
|
|
|
|
|
|
async show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { |
|
|
|
// I have no idea why I need this protection -- but it happens
|
|
|
|
if (!editor.document) return; |
|
|
|
|
|
|
|
if (this._config.statusBar.enabled && this._statusBarItem !== undefined) { |
|
|
|
switch (this._config.statusBar.date) { |
|
|
|
case 'off': |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}`; |
|
|
|
break; |
|
|
|
case 'absolute': |
|
|
|
const dateFormat = this._config.statusBar.dateFormat || 'MMMM Do, YYYY h:MMa'; |
|
|
|
let date: string; |
|
|
|
try { |
|
|
|
date = moment(commit.date).format(dateFormat); |
|
|
|
} catch (ex) { |
|
|
|
date = moment(commit.date).format('MMMM Do, YYYY h:MMa'); |
|
|
|
} |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${date}`; |
|
|
|
break; |
|
|
|
default: |
|
|
|
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
switch (this._config.statusBar.command) { |
|
|
|
case StatusBarCommand.BlameAnnotate: |
|
|
|
this._statusBarItem.tooltip = 'Toggle Blame Annotations'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowBlameHistory: |
|
|
|
this._statusBarItem.tooltip = 'Open Blame History Explorer'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowFileHistory: |
|
|
|
this._statusBarItem.tooltip = 'Open File History Explorer'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.DiffWithPrevious: |
|
|
|
this._statusBarItem.tooltip = 'Compare with Previous Commit'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ToggleCodeLens: |
|
|
|
this._statusBarItem.tooltip = 'Toggle Git CodeLens'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCommitDetails: |
|
|
|
this._statusBarItem.tooltip = 'Show Commit Details'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCommitFileDetails: |
|
|
|
this._statusBarItem.tooltip = 'Show Line Commit Details'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickFileHistory: |
|
|
|
this._statusBarItem.tooltip = 'Show File History'; |
|
|
|
break; |
|
|
|
case StatusBarCommand.ShowQuickCurrentBranchHistory: |
|
|
|
this._statusBarItem.tooltip = 'Show Branch History'; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this._statusBarItem.show(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this._config.blame.annotation.activeLine !== 'off') { |
|
|
|
const activeLine = this._config.blame.annotation.activeLine; |
|
|
|
const offset = this._uri.offset; |
|
|
|
|
|
|
|
const cfg = { |
|
|
|
annotation: { |
|
|
|
sha: true, |
|
|
|
author: this._config.statusBar.enabled ? false : this._config.blame.annotation.author, |
|
|
|
date: this._config.statusBar.enabled ? 'off' : this._config.blame.annotation.date, |
|
|
|
message: true |
|
|
|
} |
|
|
|
} as IBlameConfig; |
|
|
|
|
|
|
|
const annotation = BlameAnnotationFormatter.getAnnotation(cfg, commit, BlameAnnotationFormat.Unconstrained); |
|
|
|
|
|
|
|
// Get the full commit message -- since blame only returns the summary
|
|
|
|
let logCommit: GitCommit | undefined = undefined; |
|
|
|
if (!commit.isUncommitted) { |
|
|
|
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha); |
|
|
|
} |
|
|
|
|
|
|
|
// I have no idea why I need this protection -- but it happens
|
|
|
|
if (!editor.document) return; |
|
|
|
|
|
|
|
let hoverMessage: string | string[] | undefined = undefined; |
|
|
|
if (activeLine !== 'inline') { |
|
|
|
// If the messages match (or we couldn't find the log), then this is a possible duplicate annotation
|
|
|
|
const possibleDuplicate = !logCommit || logCommit.message === commit.message; |
|
|
|
// If we don't have a possible dupe or we aren't showing annotations get the hover message
|
|
|
|
if (!commit.isUncommitted && (!possibleDuplicate || !this.annotationController.isAnnotating(editor))) { |
|
|
|
hoverMessage = BlameAnnotationFormatter.getAnnotationHover(cfg, blameLine, logCommit || commit); |
|
|
|
|
|
|
|
// if (commit.previousSha !== undefined) {
|
|
|
|
// const changes = await this.git.getDiffForLine(this._uri.repoPath, this._uri.fsPath, blameLine.line + offset, commit.previousSha);
|
|
|
@ -310,7 +310,7 @@ export class BlameActiveLineController extends Disposable { |
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
} |
|
|
|
} |
|
|
|
else if (commit.isUncommitted) { |
|
|
|
const changes = await this.git.getDiffForLine(this._uri.repoPath, this._uri.fsPath, blameLine.line + offset); |
|
|
|
if (changes !== undefined) { |
|
|
@ -324,44 +324,62 @@ export class BlameActiveLineController extends Disposable { |
|
|
|
// }
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let decorationOptions: DecorationOptions | undefined = undefined; |
|
|
|
switch (activeLine) { |
|
|
|
case 'both': |
|
|
|
case 'inline': |
|
|
|
decorationOptions = { |
|
|
|
range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), |
|
|
|
hoverMessage: hoverMessage, |
|
|
|
renderOptions: { |
|
|
|
after: { |
|
|
|
contentText: annotation |
|
|
|
}, |
|
|
|
dark: { |
|
|
|
after: { |
|
|
|
color: this._config.blame.annotation.activeLineDarkColor || 'rgba(153, 153, 153, 0.35)' |
|
|
|
} |
|
|
|
}, |
|
|
|
light: { |
|
|
|
after: { |
|
|
|
color: this._config.blame.annotation.activeLineLightColor || 'rgba(153, 153, 153, 0.35)' |
|
|
|
} |
|
|
|
} |
|
|
|
} as DecorationInstanceRenderOptions |
|
|
|
} as DecorationOptions; |
|
|
|
break; |
|
|
|
|
|
|
|
case 'hover': |
|
|
|
decorationOptions = { |
|
|
|
range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), |
|
|
|
hoverMessage: hoverMessage |
|
|
|
} as DecorationOptions; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (decorationOptions !== undefined) { |
|
|
|
editor.setDecorations(activeLineDecoration, [decorationOptions]); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let decorationOptions: [DecorationOptions] | undefined = undefined; |
|
|
|
switch (activeLine) { |
|
|
|
case 'both': |
|
|
|
case 'inline': |
|
|
|
const range = editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)); |
|
|
|
decorationOptions = [ |
|
|
|
{ |
|
|
|
range: range.with({ |
|
|
|
start: range.start.with({ |
|
|
|
character: range.end.character |
|
|
|
}) |
|
|
|
}), |
|
|
|
hoverMessage: hoverMessage, |
|
|
|
renderOptions: { |
|
|
|
after: { |
|
|
|
contentText: annotation |
|
|
|
}, |
|
|
|
dark: { |
|
|
|
after: { |
|
|
|
color: this._config.blame.annotation.activeLineDarkColor || 'rgba(153, 153, 153, 0.35)' |
|
|
|
} |
|
|
|
}, |
|
|
|
light: { |
|
|
|
after: { |
|
|
|
color: this._config.blame.annotation.activeLineLightColor || 'rgba(153, 153, 153, 0.35)' |
|
|
|
} |
|
|
|
} |
|
|
|
} as DecorationInstanceRenderOptions |
|
|
|
} as DecorationOptions, |
|
|
|
// Add a hover decoration to the area between the start of the line and the first non-whitespace character
|
|
|
|
{ |
|
|
|
range: range.with({ |
|
|
|
end: range.end.with({ |
|
|
|
character: editor.document.lineAt(range.end.line).firstNonWhitespaceCharacterIndex |
|
|
|
}) |
|
|
|
}), |
|
|
|
hoverMessage: hoverMessage |
|
|
|
} as DecorationOptions |
|
|
|
]; |
|
|
|
break; |
|
|
|
|
|
|
|
case 'hover': |
|
|
|
decorationOptions = [ |
|
|
|
{ |
|
|
|
range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), |
|
|
|
hoverMessage: hoverMessage |
|
|
|
} as DecorationOptions |
|
|
|
]; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (decorationOptions !== undefined) { |
|
|
|
editor.setDecorations(activeLineDecoration, decorationOptions); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |