Parcourir la source

Refactors current line tracking

Splits currentLineController into:
lineAnnotationController - end of line annotation
lineHoverController - hovers to current line(s)
statusBarController - blame on the statusbar
main
Eric Amodio il y a 6 ans
Parent
révision
28b87cfc92
21 fichiers modifiés avec 1161 ajouts et 1142 suppressions
  1. +2
    -0
      CHANGELOG.md
  2. +0
    -2
      README.md
  3. +0
    -18
      package.json
  4. +8
    -8
      src/annotations/fileAnnotationController.ts
  5. +202
    -0
      src/annotations/lineAnnotationController.ts
  6. +180
    -0
      src/annotations/lineHoverController.ts
  7. +0
    -4
      src/commands.ts
  8. +1
    -1
      src/commands/clearFileAnnotations.ts
  9. +1
    -3
      src/commands/common.ts
  10. +1
    -1
      src/commands/openFileRevision.ts
  11. +1
    -1
      src/commands/openWorkingFile.ts
  12. +0
    -34
      src/commands/showFileBlame.ts
  13. +0
    -22
      src/commands/showLineBlame.ts
  14. +1
    -1
      src/commands/toggleFileBlame.ts
  15. +1
    -1
      src/commands/toggleLineBlame.ts
  16. +25
    -11
      src/container.ts
  17. +0
    -539
      src/currentLineController.ts
  18. +136
    -0
      src/statusBarController.ts
  19. +103
    -7
      src/trackers/gitLineTracker.ts
  20. +22
    -12
      src/trackers/lineTracker.ts

+ 2
- 0
CHANGELOG.md Voir le fichier

@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds `${agoOrDate}` and `${authorAgoOrDate}` tokens to `gitlens.blame.format`, `gitlens.currentLine.format`, `gitlens.explorers.commitFormat`, `gitlens.explorers.stashFormat`, and `gitlens.statusBar.format` settings which will honor the `gitlens.defaultDateStyle` setting — closes [#312](https://github.com/eamodio/vscode-gitlens/issues/312)
### Removed
- Removes the unnecessary *Show File Blame Annotations* (`gitlens.showFileBlame`) command — *Toggle File Blame Annotations* (`gitlens.toggleFileBlame`) provides similar functionality
- Removes the unnecessary *Show Line Blame Annotations* (`gitlens.showLineBlame`) command — *Toggle Line Blame Annotations* (`gitlens.toggleLineBlame`) provides similar functionality
- Removes the *Open Working File* command from the editor toolbar when the built-in *Open File* command is visible
### Fixed

+ 0
- 2
README.md Voir le fichier

@ -245,7 +245,6 @@ An on-demand, [customizable](#gitlens-results-view-settings "Jump to the GitLens
- Adds an unobtrusive, [customizable](#current-line-blame-settings "Jump to the Current Line Blame settings"), and [themable](#themable-colors "Jump to the Themable Colors"), **blame annotation** at the end of the current line
- Contains the author, date, and message of the current line's most recent commit (by [default](#current-line-blame-settings "Jump to the Current Line Blame settings"))
- Adds a *Show Line Blame Annotations* command (`gitlens.showLineBlame`)
- Adds a *Toggle Line Blame Annotations* command (`gitlens.toggleLineBlame`) to toggle the blame annotation on and off
---
@ -258,7 +257,6 @@ An on-demand, [customizable](#gitlens-results-view-settings "Jump to the GitLens
- Contains the commit message and date, by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")
- Adds a **heatmap** (age) indicator on right edge (by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")) of the gutter to provide an easy, at-a-glance way to tell the age of a line ([optional](#gutter-blame-settings "Jump to the Gutter Blame settings"), on by default)
- Indicator ranges from bright yellow (newer) to dark brown (older)
- Adds a *Show File Blame Annotations* command (`gitlens.showFileBlame`)
- Adds a *Toggle File Blame Annotations* command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the blame annotations on and off
- Press `Escape` to turn off the annotations

+ 0
- 18
package.json Voir le fichier

@ -1182,16 +1182,6 @@
"category": "GitLens"
},
{
"command": "gitlens.showFileBlame",
"title": "Show File Blame Annotations",
"category": "GitLens"
},
{
"command": "gitlens.showLineBlame",
"title": "Show Line Blame Annotations",
"category": "GitLens"
},
{
"command": "gitlens.toggleFileBlame",
"title": "Toggle File Blame Annotations",
"category": "GitLens",
@ -1750,14 +1740,6 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.showFileBlame",
"when": "gitlens:activeIsBlameable"
},
{
"command": "gitlens.showLineBlame",
"when": "gitlens:activeIsBlameable"
},
{
"command": "gitlens.toggleFileBlame",
"when": "gitlens:activeIsBlameable"
},

src/annotations/annotationController.ts → src/annotations/fileAnnotationController.ts Voir le fichier

@ -34,7 +34,7 @@ export const Decorations = {
recentChangesHighlight: undefined as TextEditorDecorationType | undefined
};
export class AnnotationController extends Disposable {
export class FileAnnotationController extends Disposable {
private _onDidToggleAnnotations = new EventEmitter<void>();
get onDidToggleAnnotations(): Event<void> {
@ -173,7 +173,7 @@ export class AnnotationController extends Disposable {
provider.reset({ decoration: Decorations.blameAnnotation, highlightDecoration: Decorations.blameHighlight });
}
else {
this.showAnnotations(provider.editor, FileAnnotationType.Heatmap);
this.show(provider.editor, FileAnnotationType.Heatmap);
}
}
}
@ -186,7 +186,7 @@ export class AnnotationController extends Disposable {
// Logger.log('AnnotationController.onActiveTextEditorChanged', editor && editor.document.uri.fsPath);
if (this.isInWindowToggle()) {
await this.showAnnotations(editor, this._annotationType!);
await this.show(editor, this._annotationType!);
return;
}
@ -296,7 +296,7 @@ export class AnnotationController extends Disposable {
return this._annotationProviders.get(AnnotationProviderBase.getCorrelationKey(editor));
}
async showAnnotations(editor: TextEditor | undefined, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
async show(editor: TextEditor | undefined, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
if (this.getToggleMode(type) === AnnotationsToggleMode.Window) {
let first = this._annotationType === undefined;
const reset = !first && this._annotationType !== type;
@ -312,7 +312,7 @@ export class AnnotationController extends Disposable {
for (const e of window.visibleTextEditors) {
if (e === editor) continue;
this.showAnnotations(e, type);
this.show(e, type);
}
}
}
@ -345,14 +345,14 @@ export class AnnotationController extends Disposable {
return provider !== undefined;
}
async toggleAnnotations(editor: TextEditor | undefined, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
async toggle(editor: TextEditor | undefined, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
if (editor !== undefined) {
const trackedDocument = await Container.tracker.getOrAdd(editor.document);
if ((type === FileAnnotationType.RecentChanges && !trackedDocument.isTracked) || !trackedDocument.isBlameable) return false;
}
const provider = this.getProvider(editor);
if (provider === undefined) return this.showAnnotations(editor!, type, shaOrLine);
if (provider === undefined) return this.show(editor!, type, shaOrLine);
const reopen = provider.annotationType !== type;
@ -365,7 +365,7 @@ export class AnnotationController extends Disposable {
if (!reopen) return false;
return this.showAnnotations(editor, type, shaOrLine);
return this.show(editor, type, shaOrLine);
}
private async attachKeyboardHook() {

+ 202
- 0
src/annotations/lineAnnotationController.ts Voir le fichier

@ -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);
}
}

+ 180
- 0
src/annotations/lineHoverController.ts Voir le fichier

@ -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;
}
}
}

+ 0
- 4
src/commands.ts Voir le fichier

@ -30,9 +30,7 @@ export * from './commands/openRepoInRemote';
export * from './commands/openWorkingFile';
export * from './commands/resetSuppressedWarnings';
export * from './commands/showCommitSearch';
export * from './commands/showFileBlame';
export * from './commands/showLastQuickPick';
export * from './commands/showLineBlame';
export * from './commands/showQuickBranchHistory';
export * from './commands/showQuickCommitDetails';
export * from './commands/showQuickCommitFileDetails';
@ -78,8 +76,6 @@ export function configureCommands(): void {
Container.context.subscriptions.push(new Commands.OpenRepoInRemoteCommand());
Container.context.subscriptions.push(new Commands.OpenWorkingFileCommand());
Container.context.subscriptions.push(new Commands.ClearFileAnnotationsCommand());
Container.context.subscriptions.push(new Commands.ShowFileBlameCommand());
Container.context.subscriptions.push(new Commands.ShowLineBlameCommand());
Container.context.subscriptions.push(new Commands.ToggleFileBlameCommand());
Container.context.subscriptions.push(new Commands.ToggleFileHeatmapCommand());
Container.context.subscriptions.push(new Commands.ToggleFileRecentChangesCommand());

+ 1
- 1
src/commands/clearFileAnnotations.ts Voir le fichier

@ -23,7 +23,7 @@ export class ClearFileAnnotationsCommand extends EditorCommand {
}
try {
return Container.annotations.clear(editor);
return Container.fileAnnotations.clear(editor);
}
catch (ex) {
Logger.error(ex, 'ClearFileAnnotationsCommand');

+ 1
- 3
src/commands/common.ts Voir le fichier

@ -40,9 +40,7 @@ export enum Commands {
OpenWorkingFile = 'gitlens.openWorkingFile',
ResetSuppressedWarnings = 'gitlens.resetSuppressedWarnings',
ShowCommitSearch = 'gitlens.showCommitSearch',
ShowFileBlame = 'gitlens.showFileBlame',
ShowLastQuickPick = 'gitlens.showLastQuickPick',
ShowLineBlame = 'gitlens.showLineBlame',
ShowQuickCommitDetails = 'gitlens.showQuickCommitDetails',
ShowQuickCommitFileDetails = 'gitlens.showQuickCommitFileDetails',
ShowQuickFileHistory = 'gitlens.showQuickFileHistory',
@ -307,7 +305,7 @@ export async function openEditor(uri: Uri, options: TextDocumentShowOptions & {
}
// TODO: revist this
// This is a bit of an ugly hack, but I added it because there a bunch of call sites and toRevisionUri isn't async (and can't be easily made async because of use in ctors)
// This is a bit of an ugly hack, but I added it because there a bunch of call sites and toRevisionUri can't be easily made async because of its use in ctors
if (uri.scheme === DocumentSchemes.GitLensGit) {
const gitUri = GitUri.fromRevisionUri(uri);
if (ImageExtensions.includes(path.extname(gitUri.fsPath))) {

+ 1
- 1
src/commands/openFileRevision.ts Voir le fichier

@ -130,7 +130,7 @@ export class OpenFileRevisionCommand extends ActiveEditorCommand {
const e = await openEditor(args.uri!, { ...args.showOptions, rethrow: true });
if (args.annotationType === undefined) return e;
return Container.annotations.showAnnotations(e!, args.annotationType, args.line);
return Container.fileAnnotations.show(e!, args.annotationType, args.line);
}
catch (ex) {
Logger.error(ex, 'OpenFileRevisionCommand');

+ 1
- 1
src/commands/openWorkingFile.ts Voir le fichier

@ -50,7 +50,7 @@ export class OpenWorkingFileCommand extends ActiveEditorCommand {
const e = await openEditor(args.uri, { ...args.showOptions, rethrow: true });
if (args.annotationType === undefined) return e;
return Container.annotations.showAnnotations(e!, args.annotationType, args.line);
return Container.fileAnnotations.show(e!, args.annotationType, args.line);
}
catch (ex) {
Logger.error(ex, 'OpenWorkingFileCommand');

+ 0
- 34
src/commands/showFileBlame.ts Voir le fichier

@ -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`);
}
}
}

+ 0
- 22
src/commands/showLineBlame.ts Voir le fichier

@ -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
- 1
src/commands/toggleFileBlame.ts Voir le fichier

@ -35,7 +35,7 @@ export class ToggleFileBlameCommand extends ActiveEditorCommand {
args = { ...args, type: FileAnnotationType.Blame };
}
return Container.annotations.toggleAnnotations(editor, args.type!, args.sha !== undefined ? args.sha : editor && editor.selection.active.line);
return Container.fileAnnotations.toggle(editor, args.type!, args.sha !== undefined ? args.sha : editor && editor.selection.active.line);
}
catch (ex) {
Logger.error(ex, 'ToggleFileBlameCommand');

+ 1
- 1
src/commands/toggleLineBlame.ts Voir le fichier

@ -12,7 +12,7 @@ export class ToggleLineBlameCommand extends ActiveEditorCommand {
async execute(editor: TextEditor, uri?: Uri): Promise<any> {
try {
return Container.lineAnnotations.toggleAnnotations(editor);
return Container.lineAnnotations.toggle(editor);
}
catch (ex) {
Logger.error(ex, 'ToggleLineBlameCommand');

+ 25
- 11
src/container.ts Voir le fichier

@ -1,9 +1,10 @@
'use strict';
import { Disposable, ExtensionContext, languages, workspace } from 'vscode';
import { AnnotationController } from './annotations/annotationController';
import { FileAnnotationController } from './annotations/fileAnnotationController';
import { CodeLensController } from './codeLensController';
import { configuration, IConfig } from './configuration';
import { CurrentLineController } from './currentLineController';
import { LineAnnotationController } from './annotations/lineAnnotationController';
import { LineHoverController } from './annotations/lineHoverController';
import { ExplorerCommands } from './views/explorerCommands';
import { GitContentProvider } from './gitContentProvider';
import { GitDocumentTracker } from './trackers/gitDocumentTracker';
@ -14,6 +15,7 @@ import { GitService } from './gitService';
import { Keyboard } from './keyboard';
import { PageProvider } from './pageProvider';
import { ResultsExplorer } from './views/resultsExplorer';
import { StatusBarController } from './statusBarController';
export class Container {
@ -28,8 +30,10 @@ export class Container {
// Since there is a bit of a chicken & egg problem with the DocumentTracker and the GitService, initialize the tracker once the GitService is loaded
this._tracker.initialize();
context.subscriptions.push(this._annotationController = new AnnotationController());
context.subscriptions.push(this._currentLineController = new CurrentLineController());
context.subscriptions.push(this._fileAnnotationController = new FileAnnotationController());
context.subscriptions.push(this._lineAnnotationController = new LineAnnotationController());
context.subscriptions.push(this._lineHoverController = new LineHoverController());
context.subscriptions.push(this._statusBarController = new StatusBarController());
context.subscriptions.push(this._codeLensController = new CodeLensController());
context.subscriptions.push(this._keyboard = new Keyboard());
context.subscriptions.push(this._pageProvider = new PageProvider());
@ -51,11 +55,6 @@ export class Container {
context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider()));
}
private static _annotationController: AnnotationController;
static get annotations() {
return this._annotationController;
}
private static _codeLensController: CodeLensController;
static get codeLens() {
return this._codeLensController;
@ -82,6 +81,11 @@ export class Container {
return this._explorerCommands;
}
private static _fileAnnotationController: FileAnnotationController;
static get fileAnnotations() {
return this._fileAnnotationController;
}
private static _git: GitService;
static get git() {
return this._git;
@ -97,9 +101,14 @@ export class Container {
return this._keyboard;
}
private static _currentLineController: CurrentLineController;
private static _lineAnnotationController: LineAnnotationController;
static get lineAnnotations() {
return this._currentLineController;
return this._lineAnnotationController;
}
private static _lineHoverController: LineHoverController;
static get lineHovers() {
return this._lineHoverController;
}
private static _lineTracker: GitLineTracker;
@ -121,6 +130,11 @@ export class Container {
return this._resultsExplorer;
}
private static _statusBarController: StatusBarController;
static get statusBar() {
return this._statusBarController;
}
private static _tracker: GitDocumentTracker;
static get tracker() {
return this._tracker;

+ 0
- 539
src/currentLineController.ts Voir le fichier

@ -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);
}
}

+ 136
- 0
src/statusBarController.ts Voir le fichier

@ -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();
}
}

+ 103
- 7
src/trackers/gitLineTracker.ts Voir le fichier

@ -1,6 +1,9 @@
'use strict';
import { Disposable, TextEditor } from 'vscode';
import { GitBlameCommit, GitLogCommit } from '../gitService';
import { LineTracker } from './lineTracker';
import { LinesChangeEvent, LineTracker } from './lineTracker';
import { Container } from '../container';
import { DocumentBlameStateChangeEvent, DocumentDirtyIdleTriggerEvent, DocumentDirtyStateChangeEvent, GitDocumentState } from './gitDocumentTracker';
export * from './lineTracker';
@ -15,21 +18,81 @@ export class GitLineState {
export class GitLineTracker extends LineTracker<GitLineState> {
private _count = 0;
private _subscriptions: Map<any, Disposable> = new Map();
start() {
if (this._disposable !== undefined) {
this._count = 0;
return;
protected async fireLinesChanged(e: LinesChangeEvent) {
this.reset();
let updated = false;
if (!this._suspended && !e.pending && e.lines !== undefined && e.editor !== undefined) {
updated = await this.updateState(e.lines, e.editor);
}
super.fireLinesChanged(updated ? e : { ...e, lines: undefined });
}
private onBlameStateChanged(e: DocumentBlameStateChangeEvent<GitDocumentState>) {
this.trigger('editor');
}
private onDirtyIdleTriggered(e: DocumentDirtyIdleTriggerEvent<GitDocumentState>) {
const maxLines = Container.config.advanced.blame.sizeThresholdAfterEdit;
if (maxLines > 0 && e.document.lineCount > maxLines) return;
this.resume();
}
private async onDirtyStateChanged(e: DocumentDirtyStateChangeEvent<GitDocumentState>) {
if (e.dirty) {
this.suspend();
}
else {
this.resume({ force: true });
}
}
private _suspended = false;
private async resume(options: { force?: boolean } = {}) {
if (!options.force && !this._suspended) return;
this._suspended = false;
this.trigger('editor');
}
private async suspend(options: { force?: boolean } = {}) {
if (!options.force && this._suspended) return;
this._suspended = true;
this.trigger('editor');
}
start(subscriber: any, subscription: Disposable): void {
if (this._subscriptions.has(subscriber)) return;
this._subscriptions.set(subscriber, subscription);
this._count++;
if (this._count === 1) {
super.start();
this._disposable = Disposable.from(
this._disposable!,
Container.tracker.onDidChangeBlameState(this.onBlameStateChanged, this),
Container.tracker.onDidChangeDirtyState(this.onDirtyStateChanged, this),
Container.tracker.onDidTriggerDirtyIdle(this.onDirtyIdleTriggered, this)
);
}
}
stop() {
if (this._disposable !== undefined) {
stop(subscriber: any) {
const subscription = this._subscriptions.get(subscriber);
if (subscription === undefined) return;
this._subscriptions.delete(subscriber);
subscription.dispose();
if (this._disposable === undefined) {
this._count = 0;
return;
}
@ -39,4 +102,37 @@ export class GitLineTracker extends LineTracker {
super.stop();
}
}
private async updateState(lines: number[], editor: TextEditor): Promise<boolean> {
const trackedDocument = await Container.tracker.getOrAdd(editor.document);
if (!trackedDocument.isBlameable || !this.includesAll(lines)) return false;
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 false;
this.setState(blameLine.line.line, new GitLineState(blameLine.commit));
}
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 false;
for (const line of lines) {
const commitLine = blame.lines[line];
this.setState(line, new GitLineState(blame.commits.get(commitLine.sha)!));
}
}
if (!trackedDocument.isBlameable || !this.includesAll(lines)) return false;
if (editor.document.isDirty) {
trackedDocument.setForceDirtyStateChangeOnNextDocumentChange();
}
return true;
}
}

+ 22
- 12
src/trackers/lineTracker.ts Voir le fichier

@ -8,7 +8,7 @@ export interface LinesChangeEvent {
readonly editor: TextEditor | undefined;
readonly lines: number[] | undefined;
readonly reason: 'editor' | 'lines';
readonly reason: 'editor' | 'selection';
readonly pending?: boolean;
}
@ -16,7 +16,6 @@ export class LineTracker extends Disposable {
private _onDidChangeActiveLines = new EventEmitter<LinesChangeEvent>();
get onDidChangeActiveLines(): Event<LinesChangeEvent> {
this._onDidChangeActiveLines.event.length;
return this._onDidChangeActiveLines.event;
}
@ -41,14 +40,14 @@ export class LineTracker extends Disposable {
this._editor = editor;
this._lines = editor !== undefined ? editor.selections.map(s => s.active.line) : undefined;
this.fireLinesChanged({ editor: editor, lines: this._lines, reason: 'editor' });
this.trigger('editor');
}
private onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) {
// If this isn't for our cached editor and its not a real editor -- kick out
if (this._editor !== e.textEditor && !isTextEditor(e.textEditor)) return;
const reason = this._editor === e.textEditor ? 'lines' : 'editor';
const reason = this._editor === e.textEditor ? 'selection' : 'editor';
const lines = e.selections.map(s => s.active.line);
if (this._editor === e.textEditor && this.includesAll(lines)) return;
@ -56,8 +55,7 @@ export class LineTracker extends Disposable {
this.reset();
this._editor = e.textEditor;
this._lines = lines;
this.fireLinesChanged({ editor: this._editor, lines: this._lines, reason: reason });
this.trigger(reason);
}
getState(line: number): T | undefined {
@ -81,11 +79,15 @@ export class LineTracker extends Disposable {
return LineTracker.includesAll(lines, this._lines);
}
refresh() {
this.trigger('editor');
}
reset() {
this._state.clear();
}
start() {
start(subscriber?: any, subscription?: Disposable) class="o">: void {
if (this._disposable !== undefined) return;
this._disposable = Disposable.from(
@ -96,7 +98,7 @@ export class LineTracker extends Disposable {
setImmediate(() => this.onActiveTextEditorChanged(window.activeTextEditor));
}
stop() {
stop(subscriber?: any) {
if (this._disposable === undefined) return;
if (this._linesChangedDebounced !== undefined) {
@ -107,9 +109,17 @@ export class LineTracker extends Disposable {
this._disposable = undefined;
}
protected async fireLinesChanged(e: LinesChangeEvent) {
this._onDidChangeActiveLines.fire(e);
}
protected trigger(reason: 'editor' | 'selection') {
this.onLinesChanged({ editor: this._editor, lines: this._lines, reason: reason });
}
private _linesChangedDebounced: (((e: LinesChangeEvent) => void) & IDeferrable) | undefined;
private fireLinesChanged(e: LinesChangeEvent) {
private onLinesChanged(e: LinesChangeEvent) {
if (e.lines === undefined) {
setImmediate(() => {
if (window.activeTextEditor !== e.editor) return;
@ -118,7 +128,7 @@ export class LineTracker extends Disposable {
this._linesChangedDebounced.cancel();
}
this._onDidChangeActiveLines.fire(e);
this.fireLinesChanged(e);
});
return;
@ -130,13 +140,13 @@ export class LineTracker extends Disposable {
// Make sure we are still on the same lines
if (!LineTracker.includesAll(e.lines , (e.editor && e.editor.selections.map(s => s.active.line)))) return;
this._onDidChangeActiveLines.fire(e);
this.fireLinesChanged(e);
}, 250, { track: true });
}
// If we have no pending moves, then fire an immediate pending event, and defer the real event
if (!this._linesChangedDebounced.pending!()) {
this._onDidChangeActiveLines.fire({ ...e, pending: true });
this.fireLinesChanged({ ...e, pending: true });
}
this._linesChangedDebounced(e);

Chargement…
Annuler
Enregistrer