Splits currentLineController into: lineAnnotationController - end of line annotation lineHoverController - hovers to current line(s) statusBarController - blame on the statusbarmain
@ -0,0 +1,202 @@ | |||||
'use strict'; | |||||
import { ConfigurationChangeEvent, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Range, TextEditor, TextEditorDecorationType, window } from 'vscode'; | |||||
import { Annotations } from './annotations'; | |||||
import { configuration, IConfig } from './../configuration'; | |||||
import { isTextEditor, RangeEndOfLineIndex } from './../constants'; | |||||
import { Container } from './../container'; | |||||
import { LinesChangeEvent } from './../trackers/gitLineTracker'; | |||||
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ | |||||
after: { | |||||
margin: '0 0 0 3em', | |||||
textDecoration: 'none' | |||||
}, | |||||
rangeBehavior: DecorationRangeBehavior.ClosedOpen | |||||
} as DecorationRenderOptions); | |||||
export class LineAnnotationController extends Disposable { | |||||
private _disposable: Disposable; | |||||
private _debugSessionEndDisposable: Disposable | undefined; | |||||
private _editor: TextEditor | undefined; | |||||
private _enabled: boolean = false; | |||||
constructor() { | |||||
super(() => this.dispose()); | |||||
this._disposable = Disposable.from( | |||||
configuration.onDidChange(this.onConfigurationChanged, this), | |||||
Container.fileAnnotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), | |||||
debug.onDidStartDebugSession(this.onDebugSessionStarted, this) | |||||
); | |||||
this.onConfigurationChanged(configuration.initializingChangeEvent); | |||||
} | |||||
dispose() { | |||||
this.clearAnnotations(this._editor); | |||||
this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose(); | |||||
Container.lineTracker.stop(this); | |||||
this._disposable && this._disposable.dispose(); | |||||
} | |||||
private onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if (!initializing && !configuration.changed(e, configuration.name('currentLine').value)) return; | |||||
if (initializing || configuration.changed(e, configuration.name('currentLine')('enabled').value)) { | |||||
const cfg = configuration.get<IConfig>(); | |||||
if (cfg.currentLine.enabled) { | |||||
this._enabled = true; | |||||
Container.lineTracker.start( | |||||
this, | |||||
Disposable.from( | |||||
Container.lineTracker.onDidChangeActiveLines(this.onActiveLinesChanged, this) | |||||
) | |||||
); | |||||
} | |||||
else { | |||||
this._enabled = false; | |||||
Container.lineTracker.stop(this); | |||||
} | |||||
} | |||||
this.refresh(window.activeTextEditor); | |||||
} | |||||
private _suspended?: 'debugging' | 'user'; | |||||
get suspended() { | |||||
return !this._enabled || this._suspended !== undefined; | |||||
} | |||||
resume(reason: 'debugging' | 'user' = 'user') { | |||||
switch (reason) { | |||||
case 'debugging': | |||||
if (this._suspended !== 'user') { | |||||
this._suspended = undefined; | |||||
return true; | |||||
} | |||||
break; | |||||
case 'user': | |||||
if (this._suspended !== undefined) { | |||||
this._suspended = undefined; | |||||
return true; | |||||
} | |||||
break; | |||||
} | |||||
return false; | |||||
} | |||||
suspend(reason: 'debugging' | 'user' = 'user') { | |||||
if (this._suspended !== 'user') { | |||||
this._suspended = reason; | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
private onActiveLinesChanged(e: LinesChangeEvent) { | |||||
if (!e.pending && e.lines !== undefined) { | |||||
this.refresh(e.editor); | |||||
return; | |||||
} | |||||
this.clear(e.editor); | |||||
} | |||||
private onDebugSessionStarted() { | |||||
if (this._debugSessionEndDisposable === undefined) { | |||||
this._debugSessionEndDisposable = debug.onDidTerminateDebugSession(this.onDebugSessionEnded, this); | |||||
} | |||||
if (this.suspend('debugging')) { | |||||
this.refresh(window.activeTextEditor); | |||||
} | |||||
} | |||||
private onDebugSessionEnded() { | |||||
if (this._debugSessionEndDisposable !== undefined) { | |||||
this._debugSessionEndDisposable.dispose(); | |||||
this._debugSessionEndDisposable = undefined; | |||||
} | |||||
if (this.resume('debugging')) { | |||||
this.refresh(window.activeTextEditor); | |||||
} | |||||
} | |||||
private onFileAnnotationsToggled() { | |||||
this.refresh(window.activeTextEditor); | |||||
} | |||||
async clear(editor: TextEditor | undefined) { | |||||
if (this._editor !== editor && this._editor !== undefined) { | |||||
this.clearAnnotations(this._editor); | |||||
} | |||||
this.clearAnnotations(editor); | |||||
} | |||||
async toggle(editor: TextEditor | undefined) { | |||||
this._enabled = !(this._enabled && !this.suspended); | |||||
if (this._enabled) { | |||||
if (this.resume('user')) { | |||||
await this.refresh(editor); | |||||
} | |||||
} | |||||
else { | |||||
if (this.suspend('user')) { | |||||
await this.refresh(editor); | |||||
} | |||||
} | |||||
} | |||||
private clearAnnotations(editor: TextEditor | undefined) { | |||||
if (editor === undefined || (editor as any)._disposed === true) return; | |||||
editor.setDecorations(annotationDecoration, []); | |||||
} | |||||
private async refresh(editor: TextEditor | undefined) { | |||||
if (editor === undefined && this._editor === undefined) return; | |||||
const lines = Container.lineTracker.lines; | |||||
if (editor === undefined || lines === undefined || !isTextEditor(editor)) return this.clear(this._editor); | |||||
if (this._editor !== editor) { | |||||
// Clear any annotations on the previously active editor | |||||
this.clear(this._editor); | |||||
this._editor = editor; | |||||
} | |||||
const cfg = Container.config.currentLine; | |||||
if (this.suspended) return this.clear(editor); | |||||
const trackedDocument = await Container.tracker.getOrAdd(editor.document); | |||||
if (!trackedDocument.isBlameable && this.suspended) return this.clear(editor); | |||||
// Make sure the editor hasn't died since the await above and that we are still on the same line(s) | |||||
if (editor.document === undefined || !Container.lineTracker.includesAll(lines)) return; | |||||
const decorations = []; | |||||
for (const l of lines) { | |||||
const state = Container.lineTracker.getState(l); | |||||
if (state === undefined || state.commit === undefined) continue; | |||||
const decoration = Annotations.trailing(state.commit, cfg.format, cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat); | |||||
decoration.range = editor.document.validateRange(new Range(l, RangeEndOfLineIndex, l, RangeEndOfLineIndex)); | |||||
decorations.push(decoration); | |||||
} | |||||
editor.setDecorations(annotationDecoration, decorations); | |||||
} | |||||
} |
@ -0,0 +1,180 @@ | |||||
'use strict'; | |||||
import { CancellationToken, ConfigurationChangeEvent, debug, Disposable, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, window } from 'vscode'; | |||||
import { Annotations } from './annotations'; | |||||
import { configuration, IConfig } from './../configuration'; | |||||
import { RangeEndOfLineIndex } from './../constants'; | |||||
import { Container } from './../container'; | |||||
import { LinesChangeEvent } from './../trackers/gitLineTracker'; | |||||
export class LineHoverController extends Disposable { | |||||
private _debugSessionEndDisposable: Disposable | undefined; | |||||
private _disposable: Disposable; | |||||
private _hoverProviderDisposable: Disposable | undefined; | |||||
constructor() { | |||||
super(() => this.dispose()); | |||||
this._disposable = Disposable.from( | |||||
configuration.onDidChange(this.onConfigurationChanged, this), | |||||
debug.onDidStartDebugSession(this.onDebugSessionStarted, this) | |||||
); | |||||
this.onConfigurationChanged(configuration.initializingChangeEvent); | |||||
} | |||||
dispose() { | |||||
this.unregister(); | |||||
this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose(); | |||||
Container.lineTracker.stop(this); | |||||
this._disposable && this._disposable.dispose(); | |||||
} | |||||
private onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if (!initializing && | |||||
!configuration.changed(e, configuration.name('hovers')('enabled').value) && | |||||
!configuration.changed(e, configuration.name('hovers')('currentLine')('enabled').value)) return; | |||||
const cfg = configuration.get<IConfig>(); | |||||
if (cfg.hovers.enabled && cfg.hovers.currentLine.enabled) { | |||||
Container.lineTracker.start( | |||||
this, | |||||
Disposable.from(Container.lineTracker.onDidChangeActiveLines(this.onActiveLinesChanged, this)) | |||||
); | |||||
this.register(window.activeTextEditor); | |||||
} | |||||
else { | |||||
Container.lineTracker.stop(this); | |||||
this.unregister(); | |||||
} | |||||
} | |||||
private get debugging() { | |||||
return this._debugSessionEndDisposable !== undefined; | |||||
} | |||||
private onActiveLinesChanged(e: LinesChangeEvent) { | |||||
if (e.pending || e.reason !== 'editor') return; | |||||
if (e.editor === undefined || e.lines === undefined) { | |||||
this.unregister(); | |||||
return; | |||||
} | |||||
this.register(e.editor); | |||||
} | |||||
private onDebugSessionStarted() { | |||||
if (this._debugSessionEndDisposable === undefined) { | |||||
this._debugSessionEndDisposable = debug.onDidTerminateDebugSession(this.onDebugSessionEnded, this); | |||||
} | |||||
} | |||||
private onDebugSessionEnded() { | |||||
if (this._debugSessionEndDisposable !== undefined) { | |||||
this._debugSessionEndDisposable.dispose(); | |||||
this._debugSessionEndDisposable = undefined; | |||||
} | |||||
} | |||||
async provideDetailsHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> { | |||||
if (!Container.lineTracker.includes(position.line)) return undefined; | |||||
const lineState = Container.lineTracker.getState(position.line); | |||||
const commit = lineState !== undefined ? lineState.commit : undefined; | |||||
if (commit === undefined) return undefined; | |||||
// Avoid double annotations if we are showing the whole-file hover blame annotations | |||||
const fileAnnotations = await Container.fileAnnotations.getAnnotationType(window.activeTextEditor); | |||||
if (fileAnnotations !== undefined && Container.config.hovers.annotations.details) return undefined; | |||||
const wholeLine = this.debugging ? false : Container.config.hovers.currentLine.over === 'line'; | |||||
// If we aren't showing the hover over the whole line, make sure the annotation is on | |||||
if (!wholeLine && Container.lineAnnotations.suspended) return undefined; | |||||
const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); | |||||
if (!wholeLine && range.start.character !== position.character) return undefined; | |||||
// Get the full commit message -- since blame only returns the summary | |||||
let logCommit = lineState !== undefined ? lineState.logCommit : undefined; | |||||
if (logCommit === undefined && !commit.isUncommitted) { | |||||
logCommit = await Container.git.getLogCommitForFile(commit.repoPath, commit.uri.fsPath, { ref: commit.sha }); | |||||
if (logCommit !== undefined) { | |||||
// Preserve the previous commit from the blame commit | |||||
logCommit.previousSha = commit.previousSha; | |||||
logCommit.previousFileName = commit.previousFileName; | |||||
if (lineState !== undefined) { | |||||
lineState.logCommit = logCommit; | |||||
} | |||||
} | |||||
} | |||||
const trackedDocument = await Container.tracker.get(document); | |||||
if (trackedDocument === undefined) return undefined; | |||||
const message = Annotations.getHoverMessage(logCommit || commit, Container.config.defaultDateFormat, await Container.git.getRemotes(commit.repoPath), fileAnnotations, position.line); | |||||
return new Hover(message, range); | |||||
} | |||||
async provideChangesHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> { | |||||
if (!Container.lineTracker.includes(position.line)) return undefined; | |||||
const lineState = Container.lineTracker.getState(position.line); | |||||
const commit = lineState !== undefined ? lineState.commit : undefined; | |||||
if (commit === undefined) return undefined; | |||||
// Avoid double annotations if we are showing the whole-file hover blame annotations | |||||
if (Container.config.hovers.annotations.changes) { | |||||
const fileAnnotations = await Container.fileAnnotations.getAnnotationType(window.activeTextEditor); | |||||
if (fileAnnotations !== undefined) return undefined; | |||||
} | |||||
const wholeLine = this.debugging ? false : Container.config.hovers.currentLine.over === 'line'; | |||||
// If we aren't showing the hover over the whole line, make sure the annotation is on | |||||
if (!wholeLine && Container.lineAnnotations.suspended) return undefined; | |||||
const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); | |||||
if (!wholeLine && range.start.character !== position.character) return undefined; | |||||
const trackedDocument = await Container.tracker.get(document); | |||||
if (trackedDocument === undefined) return undefined; | |||||
const hover = await Annotations.changesHover(commit, position.line, trackedDocument.uri); | |||||
if (hover.hoverMessage === undefined) return undefined; | |||||
return new Hover(hover.hoverMessage, range); | |||||
} | |||||
private register(editor: TextEditor | undefined) { | |||||
this.unregister(); | |||||
if (editor === undefined /* || this.suspended */) return; | |||||
const cfg = Container.config.hovers; | |||||
if (!cfg.enabled || !cfg.currentLine.enabled || (!cfg.currentLine.details && !cfg.currentLine.changes)) return; | |||||
const subscriptions = []; | |||||
if (cfg.currentLine.changes) { | |||||
subscriptions.push(languages.registerHoverProvider({ pattern: editor.document.uri.fsPath }, { provideHover: this.provideChangesHover.bind(this) } as HoverProvider)); | |||||
} | |||||
if (cfg.currentLine.details) { | |||||
subscriptions.push(languages.registerHoverProvider({ pattern: editor.document.uri.fsPath }, { provideHover: this.provideDetailsHover.bind(this) } as HoverProvider)); | |||||
} | |||||
this._hoverProviderDisposable = Disposable.from(...subscriptions); | |||||
} | |||||
private unregister() { | |||||
if (this._hoverProviderDisposable !== undefined) { | |||||
this._hoverProviderDisposable.dispose(); | |||||
this._hoverProviderDisposable = undefined; | |||||
} | |||||
} | |||||
} |
@ -1,34 +0,0 @@ | |||||
'use strict'; | |||||
import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; | |||||
import { Commands, EditorCommand } from './common'; | |||||
import { FileAnnotationType } from '../configuration'; | |||||
import { Container } from '../container'; | |||||
import { Logger } from '../logger'; | |||||
export interface ShowFileBlameCommandArgs { | |||||
sha?: string; | |||||
type?: FileAnnotationType; | |||||
} | |||||
export class ShowFileBlameCommand extends EditorCommand { | |||||
constructor() { | |||||
super(Commands.ShowFileBlame); | |||||
} | |||||
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise<any> { | |||||
if (editor === undefined) return undefined; | |||||
try { | |||||
if (args.type === undefined) { | |||||
args = { ...args, type: FileAnnotationType.Blame }; | |||||
} | |||||
return Container.annotations.showAnnotations(editor, args.type!, args.sha !== undefined ? args.sha : editor.selection.active.line); | |||||
} | |||||
catch (ex) { | |||||
Logger.error(ex, 'ShowFileBlameCommand'); | |||||
return window.showErrorMessage(`Unable to show file blame annotations. See output channel for more details`); | |||||
} | |||||
} | |||||
} |
@ -1,22 +0,0 @@ | |||||
'use strict'; | |||||
import { TextEditor, Uri, window } from 'vscode'; | |||||
import { ActiveEditorCommand, Commands } from './common'; | |||||
import { Container } from '../container'; | |||||
import { Logger } from '../logger'; | |||||
export class ShowLineBlameCommand extends ActiveEditorCommand { | |||||
constructor() { | |||||
super(Commands.ShowLineBlame); | |||||
} | |||||
async execute(editor?: TextEditor, uri?: Uri): Promise<any> { | |||||
try { | |||||
return Container.lineAnnotations.showAnnotations(editor); | |||||
} | |||||
catch (ex) { | |||||
Logger.error(ex, 'ShowLineBlameCommand'); | |||||
return window.showErrorMessage(`Unable to show line blame annotations. See output channel for more details`); | |||||
} | |||||
} | |||||
} |
@ -1,539 +0,0 @@ | |||||
'use strict'; | |||||
import { Functions, IDeferrable } from './system'; | |||||
import { CancellationToken, ConfigurationChangeEvent, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, window } from 'vscode'; | |||||
import { Annotations } from './annotations/annotations'; | |||||
import { Commands } from './commands'; | |||||
import { configuration, IConfig, StatusBarCommand } from './configuration'; | |||||
import { isTextEditor, RangeEndOfLineIndex } from './constants'; | |||||
import { Container } from './container'; | |||||
import { DocumentBlameStateChangeEvent, DocumentDirtyIdleTriggerEvent, DocumentDirtyStateChangeEvent, GitDocumentState, TrackedDocument } from './trackers/gitDocumentTracker'; | |||||
import { GitLineState, GitLineTracker, LinesChangeEvent } from './trackers/gitLineTracker'; | |||||
import { CommitFormatter, GitBlameLine, GitCommit, ICommitFormatOptions } from './gitService'; | |||||
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ | |||||
after: { | |||||
margin: '0 0 0 3em', | |||||
textDecoration: 'none' | |||||
}, | |||||
rangeBehavior: DecorationRangeBehavior.ClosedOpen | |||||
} as DecorationRenderOptions); | |||||
class AnnotationState { | |||||
constructor(private _enabled: boolean) { } | |||||
get enabled(): boolean { | |||||
return this.suspended ? false : this._enabled; | |||||
} | |||||
private _suspendReason?: 'debugging' | 'dirty'; | |||||
get suspended(): boolean { | |||||
return this._suspendReason !== undefined; | |||||
} | |||||
reset(enabled: boolean): boolean { | |||||
// returns whether a refresh is required | |||||
if (this._enabled === enabled && !this.suspended) return false; | |||||
this._enabled = enabled; | |||||
this._suspendReason = undefined; | |||||
return true; | |||||
} | |||||
resume(reason: 'debugging' | 'dirty'): boolean { | |||||
// returns whether a refresh is required | |||||
const refresh = this._suspendReason !== undefined; | |||||
this._suspendReason = undefined; | |||||
return refresh; | |||||
} | |||||
suspend(reason: 'debugging' | 'dirty'): boolean { | |||||
// returns whether a refresh is required | |||||
const refresh = this._suspendReason === undefined; | |||||
this._suspendReason = reason; | |||||
return refresh; | |||||
} | |||||
} | |||||
export class CurrentLineController extends Disposable { | |||||
private _blameAnnotationState: AnnotationState | undefined; | |||||
private _editor: TextEditor | undefined; | |||||
private _lineTracker: GitLineTracker; | |||||
private _statusBarItem: StatusBarItem | undefined; | |||||
private _disposable: Disposable; | |||||
private _debugSessionEndDisposable: Disposable | undefined; | |||||
private _hoverProviderDisposable: Disposable | undefined; | |||||
private _lineTrackingDisposable: Disposable | undefined; | |||||
constructor() { | |||||
super(() => this.dispose()); | |||||
this._lineTracker = Container.lineTracker; | |||||
this._disposable = Disposable.from( | |||||
this._lineTracker, | |||||
configuration.onDidChange(this.onConfigurationChanged, this), | |||||
Container.annotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), | |||||
debug.onDidStartDebugSession(this.onDebugSessionStarted, this) | |||||
); | |||||
this.onConfigurationChanged(configuration.initializingChangeEvent); | |||||
} | |||||
dispose() { | |||||
this.clearAnnotations(this._editor); | |||||
this.unregisterHoverProviders(); | |||||
this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose(); | |||||
this._lineTrackingDisposable && this._lineTrackingDisposable.dispose(); | |||||
this._statusBarItem && this._statusBarItem.dispose(); | |||||
this._disposable && this._disposable.dispose(); | |||||
} | |||||
private onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
const cfg = configuration.get<IConfig>(); | |||||
let changed = false; | |||||
if (initializing || configuration.changed(e, configuration.name('currentLine').value)) { | |||||
changed = true; | |||||
this._blameAnnotationState = undefined; | |||||
} | |||||
if (initializing || configuration.changed(e, configuration.name('hovers').value)) { | |||||
changed = true; | |||||
this.unregisterHoverProviders(); | |||||
} | |||||
if (initializing || configuration.changed(e, configuration.name('statusBar').value)) { | |||||
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 (this._statusBarItem !== undefined) { | |||||
this._statusBarItem.dispose(); | |||||
this._statusBarItem = undefined; | |||||
} | |||||
} | |||||
if (!changed) return; | |||||
const trackCurrentLine = cfg.currentLine.enabled || cfg.statusBar.enabled || (cfg.hovers.enabled && cfg.hovers.currentLine.enabled) || | |||||
(this._blameAnnotationState !== undefined && this._blameAnnotationState.enabled); | |||||
if (trackCurrentLine) { | |||||
this._lineTracker.start(); | |||||
this._lineTrackingDisposable = this._lineTrackingDisposable || Disposable.from( | |||||
this._lineTracker.onDidChangeActiveLines(this.onActiveLinesChanged, this), | |||||
Container.tracker.onDidChangeBlameState(this.onBlameStateChanged, this), | |||||
Container.tracker.onDidChangeDirtyState(this.onDirtyStateChanged, this), | |||||
Container.tracker.onDidTriggerDirtyIdle(this.onDirtyIdleTriggered, this) | |||||
); | |||||
} | |||||
else { | |||||
this._lineTracker.stop(); | |||||
if (this._lineTrackingDisposable !== undefined) { | |||||
this._lineTrackingDisposable.dispose(); | |||||
this._lineTrackingDisposable = undefined; | |||||
} | |||||
} | |||||
this.refresh(window.activeTextEditor, { full: true }); | |||||
} | |||||
private onActiveLinesChanged(e: LinesChangeEvent) { | |||||
if (!e.pending && e.lines !== undefined) { | |||||
this.refresh(e.editor); | |||||
return; | |||||
} | |||||
this.clear(e.editor, (Container.config.statusBar.reduceFlicker && e.reason === 'lines' && e.lines !== undefined) ? 'lines' : undefined); | |||||
} | |||||
private onBlameStateChanged(e: DocumentBlameStateChangeEvent<GitDocumentState>) { | |||||
if (e.blameable) { | |||||
this.refresh(e.editor); | |||||
return; | |||||
} | |||||
this.clear(e.editor); | |||||
} | |||||
private onDebugSessionStarted() { | |||||
if (this.suspendBlameAnnotations('debugging', window.activeTextEditor)) { | |||||
this._debugSessionEndDisposable = debug.onDidTerminateDebugSession(this.onDebugSessionEnded, this); | |||||
} | |||||
} | |||||
private onDebugSessionEnded() { | |||||
if (this._debugSessionEndDisposable !== undefined) { | |||||
this._debugSessionEndDisposable.dispose(); | |||||
this._debugSessionEndDisposable = undefined; | |||||
} | |||||
this.resumeBlameAnnotations('debugging', window.activeTextEditor); | |||||
} | |||||
private onDirtyIdleTriggered(e: DocumentDirtyIdleTriggerEvent<GitDocumentState>) { | |||||
const maxLines = configuration.get<number>(configuration.name('advanced')('blame')('sizeThresholdAfterEdit').value); | |||||
if (maxLines > 0 && e.document.lineCount > maxLines) return; | |||||
this.resumeBlameAnnotations('dirty', window.activeTextEditor); | |||||
} | |||||
private async onDirtyStateChanged(e: DocumentDirtyStateChangeEvent<GitDocumentState>) { | |||||
if (e.dirty) { | |||||
this.suspendBlameAnnotations('dirty', window.activeTextEditor); | |||||
} | |||||
else { | |||||
this.resumeBlameAnnotations('dirty', window.activeTextEditor, { force: true }); | |||||
} | |||||
} | |||||
private onFileAnnotationsToggled() { | |||||
this.refresh(window.activeTextEditor); | |||||
} | |||||
async clear(editor: TextEditor | undefined, reason?: 'lines') { | |||||
if (this._editor !== editor && this._editor !== undefined) { | |||||
this.clearAnnotations(this._editor); | |||||
} | |||||
this.clearAnnotations(editor); | |||||
this.unregisterHoverProviders(); | |||||
this._lineTracker.reset(); | |||||
if (this._statusBarItem !== undefined && reason !== 'lines') { | |||||
this._statusBarItem.hide(); | |||||
} | |||||
} | |||||
async provideDetailsHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> { | |||||
if (this._editor === undefined || this._editor.document !== document || !this._lineTracker.includes(position.line)) return undefined; | |||||
const lineState = this._lineTracker.getState(position.line); | |||||
const commit = lineState !== undefined ? lineState.commit : undefined; | |||||
if (commit === undefined) return undefined; | |||||
// Avoid double annotations if we are showing the whole-file hover blame annotations | |||||
const fileAnnotations = await Container.annotations.getAnnotationType(this._editor); | |||||
if (fileAnnotations !== undefined && Container.config.hovers.annotations.details) return undefined; | |||||
const wholeLine = Container.config.hovers.currentLine.over === 'line'; | |||||
const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); | |||||
if (!wholeLine && range.start.character !== position.character) return undefined; | |||||
// Get the full commit message -- since blame only returns the summary | |||||
let logCommit = lineState !== undefined ? lineState.logCommit : undefined; | |||||
if (logCommit === undefined && !commit.isUncommitted) { | |||||
logCommit = await Container.git.getLogCommitForFile(commit.repoPath, commit.uri.fsPath, { ref: commit.sha }); | |||||
if (logCommit !== undefined) { | |||||
// Preserve the previous commit from the blame commit | |||||
logCommit.previousSha = commit.previousSha; | |||||
logCommit.previousFileName = commit.previousFileName; | |||||
if (lineState !== undefined) { | |||||
lineState.logCommit = logCommit; | |||||
} | |||||
} | |||||
} | |||||
const trackedDocument = await Container.tracker.get(document); | |||||
if (trackedDocument === undefined) return undefined; | |||||
const message = Annotations.getHoverMessage(logCommit || commit, Container.config.defaultDateFormat, await Container.git.getRemotes(commit.repoPath), fileAnnotations, position.line); | |||||
return new Hover(message, range); | |||||
} | |||||
async provideChangesHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> { | |||||
if (this._editor === undefined || this._editor.document !== document || !this._lineTracker.includes(position.line)) return undefined; | |||||
const lineState = this._lineTracker.getState(position.line); | |||||
const commit = lineState !== undefined ? lineState.commit : undefined; | |||||
if (commit === undefined) return undefined; | |||||
// Avoid double annotations if we are showing the whole-file hover blame annotations | |||||
if (Container.config.hovers.annotations.changes) { | |||||
const fileAnnotations = await Container.annotations.getAnnotationType(this._editor); | |||||
if (fileAnnotations !== undefined) return undefined; | |||||
} | |||||
const wholeLine = Container.config.hovers.currentLine.over === 'line'; | |||||
const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); | |||||
if (!wholeLine && range.start.character !== position.character) return undefined; | |||||
const trackedDocument = await Container.tracker.get(document); | |||||
if (trackedDocument === undefined) return undefined; | |||||
const hover = await Annotations.changesHover(commit, position.line, trackedDocument.uri); | |||||
if (hover.hoverMessage === undefined) return undefined; | |||||
return new Hover(hover.hoverMessage, range); | |||||
} | |||||
async showAnnotations(editor: TextEditor | undefined) { | |||||
this.setBlameAnnotationState(true, editor); | |||||
} | |||||
async toggleAnnotations(editor: TextEditor | undefined) { | |||||
const state = this.getBlameAnnotationState(); | |||||
this.setBlameAnnotationState(!state.enabled, editor); | |||||
} | |||||
private async resumeBlameAnnotations(reason: 'debugging' | 'dirty', editor: TextEditor | undefined, options: { force?: boolean } = {}) { | |||||
if (!options.force && (this._blameAnnotationState === undefined || !this._blameAnnotationState.suspended)) return; | |||||
let refresh = false; | |||||
if (this._blameAnnotationState !== undefined) { | |||||
refresh = this._blameAnnotationState.resume(reason); | |||||
} | |||||
if (editor === undefined || (!options.force && !refresh)) return; | |||||
await this.refresh(editor); | |||||
} | |||||
private async suspendBlameAnnotations(reason: 'debugging' | 'dirty', editor: TextEditor | undefined, options: { force?: boolean } = {}) { | |||||
const state = this.getBlameAnnotationState(); | |||||
// If we aren't enabled, suspend doesn't matter | |||||
if (this._blameAnnotationState === undefined && !state.enabled) return false; | |||||
if (this._blameAnnotationState === undefined) { | |||||
this._blameAnnotationState = new AnnotationState(state.enabled); | |||||
} | |||||
const refresh = this._blameAnnotationState.suspend(reason); | |||||
if (editor === undefined || (!options.force && !refresh)) return; | |||||
await this.refresh(editor); | |||||
return true; | |||||
} | |||||
private async setBlameAnnotationState(enabled: boolean, editor: TextEditor | undefined) { | |||||
let refresh = true; | |||||
if (this._blameAnnotationState === undefined) { | |||||
this._blameAnnotationState = new AnnotationState(enabled); | |||||
} | |||||
else { | |||||
refresh = this._blameAnnotationState.reset(enabled); | |||||
} | |||||
if (editor === undefined || !refresh) return; | |||||
await this.refresh(editor); | |||||
} | |||||
private clearAnnotations(editor: TextEditor | undefined) { | |||||
if (editor === undefined || (editor as any)._disposed === true) return; | |||||
editor.setDecorations(annotationDecoration, []); | |||||
} | |||||
private getBlameAnnotationState() { | |||||
if (this._blameAnnotationState !== undefined) return this._blameAnnotationState; | |||||
const cfg = Container.config; | |||||
return { enabled: cfg.currentLine.enabled }; | |||||
} | |||||
private _updateBlameDebounced: (((lines: number[], editor: TextEditor, trackedDocument: TrackedDocument<GitDocumentState>) => void) & IDeferrable) | undefined; | |||||
private async refresh(editor: TextEditor | undefined, options: { full?: boolean, trackedDocument?: TrackedDocument<GitDocumentState> } = {}) { | |||||
if (editor === undefined && this._editor === undefined) return; | |||||
if (editor === undefined || this._lineTracker.lines === undefined) return this.clear(this._editor); | |||||
if (this._editor !== editor) { | |||||
// If we are changing editor, consider this a full refresh | |||||
options.full = true; | |||||
// Clear any annotations on the previously active editor | |||||
this.clearAnnotations(this._editor); | |||||
this._editor = editor; | |||||
} | |||||
const state = this.getBlameAnnotationState(); | |||||
if (state.enabled || Container.config.statusBar.enabled || (Container.config.hovers.enabled && Container.config.hovers.currentLine.enabled)) { | |||||
if (options.trackedDocument === undefined) { | |||||
options.trackedDocument = await Container.tracker.getOrAdd(editor.document); | |||||
} | |||||
if (options.trackedDocument.isBlameable) { | |||||
if (Container.config.hovers.enabled && Container.config.hovers.currentLine.enabled && | |||||
(options.full || this._hoverProviderDisposable === undefined)) { | |||||
this.registerHoverProviders(editor, Container.config.hovers.currentLine); | |||||
} | |||||
if (this._updateBlameDebounced === undefined) { | |||||
this._updateBlameDebounced = Functions.debounce(this.updateBlame, 50, { track: true }); | |||||
} | |||||
this._updateBlameDebounced(this._lineTracker.lines, editor, options.trackedDocument); | |||||
return; | |||||
} | |||||
} | |||||
await this.clear(editor); | |||||
} | |||||
private registerHoverProviders(editor: TextEditor | undefined, providers: { details: boolean, changes: boolean }) { | |||||
this.unregisterHoverProviders(); | |||||
if (editor === undefined) return; | |||||
if (!providers.details && !providers.changes) return; | |||||
const subscriptions: Disposable[] = []; | |||||
if (providers.changes) { | |||||
subscriptions.push(languages.registerHoverProvider({ pattern: editor.document.uri.fsPath }, { provideHover: this.provideChangesHover.bind(this) } as HoverProvider)); | |||||
} | |||||
if (providers.details) { | |||||
subscriptions.push(languages.registerHoverProvider({ pattern: editor.document.uri.fsPath }, { provideHover: this.provideDetailsHover.bind(this) } as HoverProvider)); | |||||
} | |||||
this._hoverProviderDisposable = Disposable.from(...subscriptions); | |||||
} | |||||
private unregisterHoverProviders() { | |||||
if (this._hoverProviderDisposable !== undefined) { | |||||
this._hoverProviderDisposable.dispose(); | |||||
this._hoverProviderDisposable = undefined; | |||||
} | |||||
} | |||||
private async updateBlame(lines: number[], editor: TextEditor, trackedDocument: TrackedDocument<GitDocumentState>) { | |||||
this._lineTracker.reset(); | |||||
// Make sure we are still on the same line and not pending | |||||
if (!this._lineTracker.includesAll(lines) || (this._updateBlameDebounced && this._updateBlameDebounced.pending!())) return; | |||||
let blameLines; | |||||
if (lines.length === 1) { | |||||
const blameLine = editor.document.isDirty | |||||
? await Container.git.getBlameForLineContents(trackedDocument.uri, lines[0], editor.document.getText()) | |||||
: await Container.git.getBlameForLine(trackedDocument.uri, lines[0]); | |||||
if (blameLine === undefined) return this.clear(editor); | |||||
blameLines = [blameLine]; | |||||
} | |||||
else { | |||||
const blame = editor.document.isDirty | |||||
? await Container.git.getBlameForFileContents(trackedDocument.uri, editor.document.getText()) | |||||
: await Container.git.getBlameForFile(trackedDocument.uri); | |||||
if (blame === undefined) return this.clear(editor); | |||||
blameLines = lines.map(l => { | |||||
const commitLine = blame.lines[l]; | |||||
return { | |||||
line: commitLine, | |||||
commit: blame.commits.get(commitLine.sha)! | |||||
}; | |||||
}); | |||||
} | |||||
// Make sure we are still on the same line, blameable, and not pending, after the await | |||||
if (this._lineTracker.includesAll(lines) && trackedDocument.isBlameable && !(this._updateBlameDebounced && this._updateBlameDebounced.pending!())) { | |||||
if (!this.getBlameAnnotationState().enabled) { | |||||
if (!Container.config.statusBar.enabled) return this.clear(editor); | |||||
this.clearAnnotations(editor); | |||||
} | |||||
} | |||||
const activeLine = blameLines[0]; | |||||
this._lineTracker.setState(activeLine.line.line, new GitLineState(activeLine.commit)); | |||||
// I have no idea why I need this protection -- but it happens | |||||
if (editor.document === undefined) return; | |||||
if (editor.document.isDirty) { | |||||
const trackedDocument = await Container.tracker.get(editor.document); | |||||
if (trackedDocument !== undefined) { | |||||
trackedDocument.setForceDirtyStateChangeOnNextDocumentChange(); | |||||
} | |||||
} | |||||
this.updateStatusBar(activeLine.commit, editor); | |||||
this.updateTrailingAnnotations(blameLines, editor); | |||||
} | |||||
private updateStatusBar(commit: GitCommit, editor: TextEditor) { | |||||
const cfg = Container.config.statusBar; | |||||
if (!cfg.enabled || this._statusBarItem === undefined || !isTextEditor(editor)) return; | |||||
this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { | |||||
truncateMessageAtNewLine: true, | |||||
dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat | |||||
} as ICommitFormatOptions)}`; | |||||
switch (cfg.command) { | |||||
case StatusBarCommand.ToggleFileBlame: | |||||
this._statusBarItem.tooltip = 'Toggle Blame Annotations'; | |||||
break; | |||||
case StatusBarCommand.DiffWithPrevious: | |||||
this._statusBarItem.command = Commands.DiffLineWithPrevious; | |||||
this._statusBarItem.tooltip = 'Compare Line Revision with Previous'; | |||||
break; | |||||
case StatusBarCommand.DiffWithWorking: | |||||
this._statusBarItem.command = Commands.DiffLineWithWorking; | |||||
this._statusBarItem.tooltip = 'Compare Line Revision with Working'; | |||||
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(); | |||||
} | |||||
private async updateTrailingAnnotations(lines: GitBlameLine[], editor: TextEditor) { | |||||
const cfg = Container.config.currentLine; | |||||
if (!this.getBlameAnnotationState().enabled || !isTextEditor(editor)) return this.clearAnnotations(editor); | |||||
const decorations = []; | |||||
for (const l of lines) { | |||||
const line = l.line.line; | |||||
const decoration = Annotations.trailing(l.commit, cfg.format, cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat); | |||||
decoration.range = editor.document.validateRange(new Range(line, RangeEndOfLineIndex, line, RangeEndOfLineIndex)); | |||||
decorations.push(decoration); | |||||
} | |||||
editor.setDecorations(annotationDecoration, decorations); | |||||
} | |||||
} |
@ -0,0 +1,136 @@ | |||||
'use strict'; | |||||
import { ConfigurationChangeEvent, Disposable, StatusBarAlignment, StatusBarItem, TextEditor, window } from 'vscode'; | |||||
import { Commands } from './commands'; | |||||
import { configuration, IConfig, StatusBarCommand } from './configuration'; | |||||
import { isTextEditor } from './constants'; | |||||
import { Container } from './container'; | |||||
import { LinesChangeEvent } from './trackers/gitLineTracker'; | |||||
import { CommitFormatter, GitCommit, ICommitFormatOptions } from './gitService'; | |||||
export class StatusBarController extends Disposable { | |||||
private _disposable: Disposable; | |||||
private _statusBarItem: StatusBarItem | undefined; | |||||
constructor() { | |||||
super(() => this.dispose()); | |||||
this._disposable = Disposable.from( | |||||
configuration.onDidChange(this.onConfigurationChanged, this) | |||||
); | |||||
this.onConfigurationChanged(configuration.initializingChangeEvent); | |||||
} | |||||
dispose() { | |||||
this.clear(); | |||||
this._statusBarItem && this._statusBarItem.dispose(); | |||||
Container.lineTracker.stop(this); | |||||
this._disposable && this._disposable.dispose(); | |||||
} | |||||
private onConfigurationChanged(e: ConfigurationChangeEvent) { | |||||
const initializing = configuration.initializing(e); | |||||
if (!initializing && !configuration.changed(e, configuration.name('statusBar').value)) return; | |||||
const cfg = configuration.get<IConfig>(); | |||||
if (cfg.statusBar.enabled) { | |||||
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; | |||||
if (configuration.changed(e, configuration.name('statusBar')('alignment').value)) { | |||||
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; | |||||
if (initializing || configuration.changed(e, configuration.name('statusBar')('enabled').value)) { | |||||
Container.lineTracker.start( | |||||
this, | |||||
Disposable.from(Container.lineTracker.onDidChangeActiveLines(this.onActiveLinesChanged, this)) | |||||
); | |||||
} | |||||
} | |||||
else { | |||||
if (configuration.changed(e, configuration.name('statusBar')('enabled').value)) { | |||||
Container.lineTracker.stop(this); | |||||
if (this._statusBarItem !== undefined) { | |||||
this._statusBarItem.dispose(); | |||||
this._statusBarItem = undefined; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
private onActiveLinesChanged(e: LinesChangeEvent) { | |||||
// If we need to reduceFlicker, don't clear if only the selected lines changed | |||||
let clear = !(Container.config.statusBar.reduceFlicker && e.reason === 'selection' && (e.pending || e.lines !== undefined)); | |||||
if (!e.pending && e.lines !== undefined) { | |||||
const state = Container.lineTracker.getState(e.lines[0]); | |||||
if (state !== undefined && state.commit !== undefined) { | |||||
this.updateStatusBar(state.commit, e.editor!); | |||||
return; | |||||
} | |||||
clear = true; | |||||
} | |||||
if (clear) { | |||||
this.clear(); | |||||
} | |||||
} | |||||
async clear() { | |||||
if (this._statusBarItem !== undefined) { | |||||
this._statusBarItem.hide(); | |||||
} | |||||
} | |||||
private updateStatusBar(commit: GitCommit, editor: TextEditor) { | |||||
const cfg = Container.config.statusBar; | |||||
if (!cfg.enabled || this._statusBarItem === undefined || !isTextEditor(editor)) return; | |||||
this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { | |||||
truncateMessageAtNewLine: true, | |||||
dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat | |||||
} as ICommitFormatOptions)}`; | |||||
switch (cfg.command) { | |||||
case StatusBarCommand.ToggleFileBlame: | |||||
this._statusBarItem.tooltip = 'Toggle Blame Annotations'; | |||||
break; | |||||
case StatusBarCommand.DiffWithPrevious: | |||||
this._statusBarItem.command = Commands.DiffLineWithPrevious; | |||||
this._statusBarItem.tooltip = 'Compare Line Revision with Previous'; | |||||
break; | |||||
case StatusBarCommand.DiffWithWorking: | |||||
this._statusBarItem.command = Commands.DiffLineWithWorking; | |||||
this._statusBarItem.tooltip = 'Compare Line Revision with Working'; | |||||
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(); | |||||
} | |||||
} |