Browse Source

Adds blame information in the statusBar

Add new StatusBar settings -- see **Extension Settings** above for details
Renames the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings options (to align with command names)
Adds new `gitlens.diffWithPrevious` option to the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings
Fixes Diff with Previous when the selection is uncommited
Removes `gitlens.blame.annotation.useCodeActions` setting and behavior
main
Eric Amodio 8 years ago
parent
commit
834b4904db
13 changed files with 606 additions and 467 deletions
  1. +13
    -3
      README.md
  2. +36
    -17
      package.json
  3. +39
    -75
      src/blameAnnotationController.ts
  4. +111
    -0
      src/blameStatusBarController.ts
  5. +58
    -36
      src/commands.ts
  6. +21
    -5
      src/configuration.ts
  7. +2
    -4
      src/constants.ts
  8. +12
    -9
      src/extension.ts
  9. +11
    -2
      src/git/git.ts
  10. +0
    -51
      src/gitCodeActionProvider.ts
  11. +17
    -3
      src/gitCodeLensProvider.ts
  12. +39
    -15
      src/gitProvider.ts

+ 13
- 3
README.md View File

@ -25,14 +25,15 @@ Must be using Git and it must be in your path.
|`gitlens.blame.annotation.sha`|Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` annotation style
|`gitlens.blame.annotation.author`|Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` annotation style
|`gitlens.blame.annotation.date`|Specifies whether the commit date will be shown in the blame annotations. Applies only to the `expanded` annotation style
|`gitlens.blame.annotation.useCodeActions`|Specifies whether code actions (Diff with Working, Diff with Previous) will be provided for the selected line, when annotating. Not required as context menu options are always provided
|`gitlens.codeLens.visibility`|Specifies when CodeLens will be triggered in the active document. `auto` - automatically. `ondemand` - only when requested. `off` - disables all active document CodeLens
|`gitlens.codeLens.location`|Specifies where CodeLens will be rendered in the active document. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `gitlens.codeLens.locationCustomSymbols`
|`gitlens.codeLens.locationCustomSymbols`|Specifies the set of document symbols to render active document CodeLens on. Must be a member of `SymbolKind`
|`gitlens.codeLens.recentChange.enabled`|Specifies whether the recent change CodeLens is shown
|`gitlens.codeLens.recentChange.command`|Specifies the command executed when the recent change CodeLens is clicked. `blame.annotate` - toggles blame annotations. `blame.explorer` - opens the blame explorer. `git.history` - opens a file history picker, which requires the Git History (git log) extension
|`gitlens.codeLens.recentChange.command`|Specifies the command executed when the recent change CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension
|`gitlens.codeLens.authors.enabled`|Specifies whether the authors CodeLens is shown
|`gitlens.codeLens.authors.command`|Specifies the command executed when the authors CodeLens is clicked. `blame.annotate` - toggles blame annotations. `blame.explorer` - opens the blame explorer. `git.history` - opens a file history picker, which requires the Git History (git log) extension
|`gitlens.codeLens.authors.command`|Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension
|`gitlens.statusBar.enabled`|Specifies whether blame information is shown in the status bar
|`gitlens.statusBar.command`|"Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension"
## Known Issues
@ -43,6 +44,15 @@ Must be using Git and it must be in your path.
## Release Notes
### 0.5.0
- Adds blame information in the statusBar
- Add new StatusBar settings -- see **Extension Settings** above for details
- Renames the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings options (to align with command names)
- Adds new `gitlens.diffWithPrevious` option to the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings
- Fixes Diff with Previous when the selection is uncommited
- Removes `gitlens.blame.annotation.useCodeActions` setting and behavior
### 0.3.3
- Fixes [#7](https://github.com/eamodio/vscode-gitlens/issues/7) - missing spawn-rx dependency (argh!)

+ 36
- 17
package.json View File

@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "0.3.3",
"version": "0.5.0",
"author": {
"name": "Eric Amodio",
"email": "eamodio@gmail.com"
@ -62,11 +62,6 @@
"default": false,
"description": "Specifies whether the commit date will be shown in the blame annotations. Applies only to the `expanded` annotation style"
},
"gitlens.blame.annotation.useCodeActions": {
"type": "boolean",
"default": false,
"description": "Specifies whether code actions (Diff with Working, Diff with Previous) will be provided for the selected line, when annotating. Not required as context menu options are always provided"
},
"gitlens.codeLens.visibility": {
"type": "string",
"default": "auto",
@ -99,13 +94,14 @@
},
"gitlens.codeLens.recentChange.command": {
"type": "string",
"default": "blame.explorer",
"default": "gitlens.showBlameHistory",
"enum": [
"blame.annotate",
"blame.explorer",
"git.history"
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.diffWithPrevious",
"git.viewFileHistory"
],
"description": "Specifies the command executed when the recent change CodeLens is clicked. `blame.annotate` - toggles blame annotations. `blame.explorer` - opens the blame explorer. `git.history` - opens a file history picker, which requires the Git History (git log) extension"
"description": "Specifies the command executed when the recent change CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension"
},
"gitlens.codeLens.authors.enabled": {
"type": "boolean",
@ -114,13 +110,31 @@
},
"gitlens.codeLens.authors.command": {
"type": "string",
"default": "blame.annotate",
"default": "gitlens.toggleBlame",
"enum": [
"blame.annotate",
"blame.explorer",
"git.history"
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.diffWithPrevious",
"git.viewFileHistory"
],
"description": "Specifies the command executed when the authors CodeLens is clicked. `blame.annotate` - toggles blame annotations. `blame.explorer` - opens the blame explorer. `git.history` - opens a file history picker, which requires the Git History (git log) extension"
"description": "Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension"
},
"gitlens.statusBar.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether blame information is shown in the status bar"
},
"gitlens.statusBar.command": {
"type": "string",
"default": "gitlens.toggleBlame",
"enum": [
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.diffWithPrevious",
"gitlens.toggleCodeLens",
"git.viewFileHistory"
],
"description": "Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `git.viewFileHistory` - opens a file history picker, which requires the Git History (git log) extension"
},
"gitlens.advanced.caching.enabled": {
"type": "boolean",
@ -153,6 +167,11 @@
"command": "gitlens.toggleCodeLens",
"title": "Git: Toggle CodeLens",
"category": "GitLens"
},
{
"command": "gitlens.showBlameHistory",
"title": "Git: Open Blame History",
"category": "GitLens"
}],
"menus": {
"editor/title": [{
@ -212,6 +231,6 @@
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"pack": "git clean -xdf && npm install && vsce package",
"pub": "git clean -xdf && npm install && vsce publish"
"pub": "git clean -xdf --exclude=node_modules/ && npm install && vsce publish"
}
}

src/gitBlameController.ts → src/blameAnnotationController.ts View File

@ -1,10 +1,8 @@
'use strict'
import {commands, DecorationInstanceRenderOptions, DecorationOptions, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, ExtensionContext, languages, OverviewRulerLane, Position, Range, TextDocument, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode';
import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace} from 'vscode';
import {BuiltInCommands, Commands, DocumentSchemes} from './constants';
import {BlameAnnotationStyle, IBlameConfig} from './configuration';
import GitProvider, {IGitBlame, IGitCommit} from './gitProvider';
import GitCodeActionsProvider from './gitCodeActionProvider';
import {DiagnosticCollectionName, DiagnosticSource} from './constants';
import * as moment from 'moment';
const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
@ -14,12 +12,9 @@ const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorat
});
let highlightDecoration: TextEditorDecorationType;
export default class GitBlameController extends Disposable {
private _controller: GitBlameEditorController|null;
export default class BlameAnnotationController extends Disposable {
private _disposable: Disposable;
private _blameDecoration: TextEditorDecorationType;
private _highlightDecoration: TextEditorDecorationType;
private _editorController: EditorBlameAnnotationController|null;
constructor(private context: ExtensionContext, private git: GitProvider) {
super(() => this.dispose());
@ -50,7 +45,7 @@ export default class GitBlameController extends Disposable {
// }));
workspace.onDidCloseTextDocument(d => {
if (!this._controller || this._controller.uri.toString() !== d.uri.toString()) return;
if (!this._editorController || this._editorController.uri.toString() !== d.uri.toString()) return;
this.clear();
})
@ -63,38 +58,37 @@ export default class GitBlameController extends Disposable {
}
clear() {
this._controller && this._controller.dispose();
this._controller = null;
this._editorController && this._editorController.dispose();
this._editorController = null;
}
showBlame(editor: TextEditor, sha?: string) {
if (!editor) {
showBlameAnnotation(editor: TextEditor, sha?: string) {
if (!editor || !editor.document || editor.document.isUntitled) {
this.clear();
return;
}
if (!this._controller) {
this._controller = new GitBlameEditorController(this.context, this.git, editor);
return this._controller.applyBlame(sha);
if (!this._editorController) {
this._editorController = new EditorBlameAnnotationController(this.context, this.git, editor);
return this._editorController.applyBlameAnnotation(sha);
}
}
toggleBlame(editor: TextEditor, sha?: string) {
if (!editor || this._controller) {
toggleBlameAnnotation(editor: TextEditor, sha?: string) {
if (!editor ||!editor.document || editor.document.isUntitled || this._editorController) {
this.clear();
return;
}
return this.showBlame(editor, sha);
return this.showBlameAnnotation(editor, sha);
}
}
class GitBlameEditorController extends Disposable {
class EditorBlameAnnotationController extends Disposable {
public uri: Uri;
private _blame: Promise<IGitBlame>;
private _config: IBlameConfig;
private _diagnostics: DiagnosticCollection;
private _disposable: Disposable;
private _document: TextDocument;
private _toggleWhitespace: boolean;
@ -104,36 +98,14 @@ class GitBlameEditorController extends Disposable {
this._document = this.editor.document;
this.uri = this._document.uri;
const fileName = this.uri.fsPath;
this._blame = this.git.getBlameForFile(fileName);
this._blame = this.git.getBlameForFile(this.uri.fsPath);
this._config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
const subscriptions: Disposable[] = [];
if (this._config.annotation.useCodeActions) {
this._diagnostics = languages.createDiagnosticCollection(DiagnosticCollectionName);
subscriptions.push(this._diagnostics);
subscriptions.push(languages.registerCodeActionsProvider(GitCodeActionsProvider.selector, new GitCodeActionsProvider(this.context, this.git)));
}
subscriptions.push(window.onDidChangeTextEditorSelection(e => {
const activeLine = e.selections[0].active.line;
this._diagnostics && this._diagnostics.clear();
this.git.getBlameForLine(e.textEditor.document.fileName, activeLine)
.then(blame => {
if (!blame) return;
// Add the bogus diagnostics to provide code actions for this sha
this._diagnostics && this._diagnostics.set(editor.document.uri, [this._getDiagnostic(editor, activeLine, blame.commit.sha)]);
this.applyHighlight(blame.commit.sha);
});
}));
subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this));
this._disposable = Disposable.from(...subscriptions);
}
@ -152,13 +124,12 @@ class GitBlameEditorController extends Disposable {
this._disposable && this._disposable.dispose();
}
_getDiagnostic(editor, line, sha) {
const diag = new Diagnostic(editor.document.validateRange(new Range(line, 0, line, 1000000)), `Diff commit ${sha}`, DiagnosticSeverity.Hint);
diag.source = DiagnosticSource;
return diag;
private _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line)
.then(blame => blame && this.applyHighlight(blame.commit.sha));
}
applyBlame(sha?: string) {
applyBlameAnnotation(sha?: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
@ -185,24 +156,29 @@ class GitBlameEditorController extends Disposable {
sha = sha || blame.commits.values().next().value.sha;
if (this._diagnostics) {
// Add the bogus diagnostics to provide code actions for this sha
const activeLine = this.editor.selection.active.line;
this._diagnostics.clear();
this._diagnostics.set(this.editor.document.uri, [this._getDiagnostic(this.editor, activeLine, sha)]);
}
return this.applyHighlight(sha);
});
}
_getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
applyHighlight(sha: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
});
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let count = 0;
let lastSha;
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [commit.message, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`];
let hoverMessage: string | Array<string> = [`_${l.sha}_: ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`];
if (l.sha.startsWith('00000000')) {
color = 'rgba(0, 188, 242, 0.6)';
@ -242,7 +218,7 @@ class GitBlameEditorController extends Disposable {
});
}
_getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] {
private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let width = 0;
if (this._config.annotation.sha) {
width += 5;
@ -283,7 +259,7 @@ class GitBlameEditorController extends Disposable {
});
}
_getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) {
private _getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) {
if (!force && !this._config.annotation.author) return '';
if (commit.author.length > max) {
return `${commit.author.substring(0, max - 1)}\\2026`;
@ -291,12 +267,12 @@ class GitBlameEditorController extends Disposable {
return commit.author;
}
_getDate(commit: IGitCommit, force?: boolean) {
private _getDate(commit: IGitCommit, force?: boolean) {
if (!force && !this._config.annotation.date) return '';
return moment(commit.date).format('MM/DD/YYYY');
}
_getGutter(commit: IGitCommit) {
private _getGutter(commit: IGitCommit) {
const author = this._getAuthor(commit);
const date = this._getDate(commit);
if (this._config.annotation.sha) {
@ -307,16 +283,4 @@ class GitBlameEditorController extends Disposable {
return author;
}
}
applyHighlight(sha: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
});
}
}

+ 111
- 0
src/blameStatusBarController.ts View File

@ -0,0 +1,111 @@
'use strict'
import {Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextEditor, window, workspace} from 'vscode';
import {IConfig, IStatusBarConfig, StatusBarCommand} from './configuration';
import {WorkspaceState} from './constants';
import GitProvider, {IGitBlameLine} from './gitProvider';
import * as moment from 'moment';
const isEqual = require('lodash.isequal');
export default class BlameStatusBarController extends Disposable {
private _config: IStatusBarConfig;
private _disposable: Disposable;
private _statusBarItem: StatusBarItem|null;
private _statusBarDisposable: Disposable|null;
constructor(private context: ExtensionContext, private git: GitProvider) {
super(() => this.dispose());
this._onConfigure();
const subscriptions: Disposable[] = [];
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigure, this));
this._disposable = Disposable.from(...subscriptions);
}
dispose() {
this._statusBarDisposable && this._statusBarDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
this._disposable && this._disposable.dispose();
}
private _onConfigure() {
const config = workspace.getConfiguration('').get<IConfig>('gitlens');
if (!isEqual(config.statusBar, this._config)) {
this._statusBarDisposable && this._statusBarDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
if (config.statusBar.enabled) {
this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 1000);
switch (config.statusBar.command) {
case StatusBarCommand.ToggleCodeLens:
if (config.codeLens.visibility !== 'ondemand') {
config.statusBar.command = StatusBarCommand.BlameAnnotate;
}
break;
case StatusBarCommand.GitViewHistory:
if (!this.context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false)) {
config.statusBar.command = StatusBarCommand.BlameExplorer;
}
break;
}
this._statusBarItem.command = config.statusBar.command;
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveSelectionChanged, this));
subscriptions.push(window.onDidChangeTextEditorSelection(e => this._onActiveSelectionChanged(e.textEditor)));
this._statusBarDisposable = Disposable.from(...subscriptions);
} else {
this._statusBarDisposable = null;
this._statusBarItem = null;
}
}
this._config = config.statusBar;
}
private _onActiveSelectionChanged(editor: TextEditor) : void {
if (!editor || !editor.document || editor.document.isUntitled) {
this.clear();
return;
}
this.git.getBlameForLine(editor.document.uri.fsPath, editor.selection.active.line)
.then(blame => blame ? this.show(blame) : this.clear());
}
clear() {
this._statusBarItem && this._statusBarItem.hide();
}
show(blameLine: IGitBlameLine) {
const commit = blameLine.commit;
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`;
//this._statusBarItem.tooltip = [`Last changed by ${commit.author}`, moment(commit.date).format('MMMM Do, YYYY h:MM a'), '', commit.message].join('\n');
switch (this._config.command) {
case StatusBarCommand.BlameAnnotate:
this._statusBarItem.tooltip = 'Toggle Blame Annotations';
break;
case StatusBarCommand.BlameExplorer:
this._statusBarItem.tooltip = 'Open Blame History';
break;
case StatusBarCommand.DiffWithPrevious:
this._statusBarItem.tooltip = 'Compare to Previous Commit';
break;
case StatusBarCommand.ToggleCodeLens:
this._statusBarItem.tooltip = 'Toggle Blame CodeLens';
break;
case StatusBarCommand.GitViewHistory:
this._statusBarItem.tooltip = 'View Git File History';
break;
}
this._statusBarItem.show();
}
}

+ 58
- 36
src/commands.ts View File

@ -2,7 +2,7 @@
import {commands, DecorationOptions, Disposable, OverviewRulerLane, Position, Range, TextEditor, TextEditorEdit, TextEditorDecorationType, Uri, window} from 'vscode';
import {BuiltInCommands, Commands} from './constants';
import GitProvider from './gitProvider';
import GitBlameController from './gitBlameController';
import BlameAnnotationController from './blameAnnotationController';
import * as moment from 'moment';
import * as path from 'path';
@ -36,8 +36,6 @@ abstract class EditorCommand extends Disposable {
abstract execute(editor: TextEditor, edit: TextEditorEdit, ...args): any;
}
const UncommitedRegex = /^[0]+$/;
export class DiffWithPreviousCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.DiffWithPrevious);
@ -46,15 +44,29 @@ export class DiffWithPreviousCommand extends EditorCommand {
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, compareWithSha?: string, compareWithUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', 'getBlameForLine', ex))
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
if (UncommitedRegex.test(blame.commit.sha)) {
return commands.executeCommand(Commands.DiffWithWorking, uri, blame.commit.repoPath, blame.commit.previousSha, blame.commit.previousUri, line);
// If the line is uncommitted, find the previous commit
const commit = blame.commit;
if (GitProvider.isUncommitted(commit.sha)) {
return this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine, commit.previousSha, commit.repoPath)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex))
.then(prevBlame => {
if (!prevBlame) return;
const prevCommit = prevBlame.commit;
return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine);
});
}
return commands.executeCommand(Commands.DiffWithPrevious, uri, blame.commit.repoPath, blame.commit.sha, blame.commit.uri, blame.commit.previousSha, blame.commit.previousUri, line);
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line);
});
}
@ -77,39 +89,50 @@ export class DiffWithWorkingCommand extends EditorCommand {
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getBlameForLine', ex))
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
if (UncommitedRegex.test(blame.commit.sha)) {
return commands.executeCommand(Commands.DiffWithWorking, uri, blame.commit.repoPath, blame.commit.previousSha, blame.commit.previousUri, line);
const commit = blame.commit;
// If the line is uncommitted, find the previous commit
if (GitProvider.isUncommitted(commit.sha)) {
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line);
}
return commands.executeCommand(Commands.DiffWithWorking, uri, blame.commit.repoPath, blame.commit.sha, blame.commit.uri, line)
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line)
});
};
return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex))
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)} (index)`)
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)}`)
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, {lineNumber: line, at: 'center'})));
}
}
export class ShowBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private blameController: GitBlameController) {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.ShowBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.blameController.toggleBlame(editor, sha);
return this.annotationController.toggleBlameAnnotation(editor, sha);
}
const activeLine = editor.selection.active.line;
return this.git.getBlameForLine(editor.document.fileName, activeLine)
.catch(ex => console.error('[GitLens.ShowBlameCommand]', 'getBlameForLine', ex))
.then(blame => this.blameController.showBlame(editor, blame && blame.commit.sha));
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha));
}
}
@ -119,40 +142,39 @@ export class ShowBlameHistoryCommand extends EditorCommand {
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) {
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
if (!uri) {
const doc = editor.document;
if (doc) {
uri = doc.uri;
range = doc.validateRange(new Range(0, 0, 1000000, 1000000));
position = doc.validateRange(new Range(0, 0, 0, 1000000)).start;
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
if (!uri) return;
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000));
position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start;
}
return this.git.getBlameLocations(uri.fsPath, range)
.catch(ex => console.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex))
.then(locations => {
return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations);
});
.then(locations => commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations));
}
}
export class ToggleBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private blameController: GitBlameController) {
constructor(private git: GitProvider, private blameController: BlameAnnotationController) {
super(Commands.ToggleBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.blameController.toggleBlame(editor, sha);
return this.blameController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
const activeLine = editor.selection.active.line;
return this.git.getBlameForLine(editor.document.fileName, activeLine)
.catch(ex => console.error('[GitLens.ToggleBlameCommand]', 'getBlameForLine', ex))
.then(blame => this.blameController.toggleBlame(editor, blame && blame.commit.sha));
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.blameController.toggleBlameAnnotation(editor, blame && blame.commit.sha));
}
}

+ 21
- 5
src/configuration.ts View File

@ -1,4 +1,5 @@
'use strict'
import {Commands} from './constants';
export type BlameAnnotationStyle = 'compact' | 'expanded';
export const BlameAnnotationStyle = {
@ -12,15 +13,15 @@ export interface IBlameConfig {
sha: boolean;
author: boolean;
date: boolean;
useCodeActions: boolean;
};
}
export type CodeLensCommand = 'blame.annotate' | 'blame.explorer' | 'git.history';
export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory';
export const CodeLensCommand = {
BlameAnnotate: 'blame.annotate' as CodeLensCommand,
BlameExplorer: 'blame.explorer' as CodeLensCommand,
GitHistory: 'git.history' as CodeLensCommand
BlameAnnotate: Commands.ToggleBlame as CodeLensCommand,
BlameExplorer: Commands.ShowBlameHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand,
GitViewHistory: 'git.viewFileHistory' as CodeLensCommand
}
export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom';
@ -51,6 +52,20 @@ export interface ICodeLensesConfig {
authors: ICodeLensConfig;
}
export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory';
export const StatusBarCommand = {
BlameAnnotate: Commands.ToggleBlame as StatusBarCommand,
BlameExplorer: Commands.ShowBlameHistory as StatusBarCommand,
DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand,
ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand,
GitViewHistory: 'git.viewFileHistory' as StatusBarCommand
}
export interface IStatusBarConfig {
enabled: boolean;
command: StatusBarCommand;
}
export interface IAdvancedConfig {
caching: {
enabled: boolean
@ -60,5 +75,6 @@ export interface IAdvancedConfig {
export interface IConfig {
blame: IBlameConfig,
codeLens: ICodeLensesConfig,
statusBar: IStatusBarConfig,
advanced: IAdvancedConfig
}

+ 2
- 4
src/constants.ts View File

@ -1,7 +1,5 @@
'use strict'
export const DiagnosticCollectionName = 'gitlens';
export const DiagnosticSource = 'GitLens';
export const RepoPath = 'repoPath';
export type BuiltInCommands = 'cursorMove' | 'editor.action.showReferences' | 'editor.action.toggleRenderWhitespace' | 'editorScroll' | 'revealLine' | 'vscode.diff' | 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider';
@ -16,12 +14,12 @@ export const BuiltInCommands = {
ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands
}
export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
export const Commands = {
DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands,
DiffWithWorking: 'gitlens.diffWithWorking' as Commands,
ShowBlame: 'gitlens.showBlame' as Commands,
ShowBlameHistory: 'gitlens.showHistory' as Commands,
ShowBlameHistory: 'gitlens.showBlameHistory' as Commands,
ToggleBlame: 'gitlens.toggleBlame' as Commands,
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands,
}

+ 12
- 9
src/extension.ts View File

@ -1,12 +1,13 @@
'use strict';
import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, OverviewRulerLane, window, workspace} from 'vscode';
import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, OverviewRulerLane, StatusBarAlignment, window, workspace} from 'vscode';
import BlameAnnotationController from './blameAnnotationController';
import BlameStatusBarController from './blameStatusBarController';
import GitContentProvider from './gitContentProvider';
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
import GitBlameContentProvider from './gitBlameContentProvider';
import GitBlameController from './gitBlameController';
import GitProvider, {Git} from './gitProvider';
import {DiffWithPreviousCommand, DiffWithWorkingCommand, ShowBlameCommand, ShowBlameHistoryCommand, ToggleBlameCommand, ToggleCodeLensCommand} from './commands';
import {ICodeLensesConfig} from './configuration';
import {IStatusBarConfig} from './configuration';
import {WorkspaceState} from './constants';
// this method is called when your extension is activated
@ -33,18 +34,20 @@ export function activate(context: ExtensionContext) {
context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git)));
const blameController = new GitBlameController(context, git);
context.subscriptions.push(blameController);
const annotationController = new BlameAnnotationController(context, git);
context.subscriptions.push(annotationController);
const statusBarController = new BlameStatusBarController(context, git);
context.subscriptions.push(statusBarController);
context.subscriptions.push(new DiffWithWorkingCommand(git));
context.subscriptions.push(new DiffWithPreviousCommand(git));
context.subscriptions.push(new ShowBlameCommand(git, blameController));
context.subscriptions.push(new ToggleBlameCommand(git, blameController));
context.subscriptions.push(new ShowBlameCommand(git, annotationController));
context.subscriptions.push(new ToggleBlameCommand(git, annotationController));
context.subscriptions.push(new ShowBlameHistoryCommand(git));
context.subscriptions.push(new ToggleCodeLensCommand(git));
}).catch(reason => console.warn('[GitLens]', reason));
}
// this method is called when your extension is deactivated
export function deactivate() {
}
export function deactivate() { }

+ 11
- 2
src/git/git.ts View File

@ -53,7 +53,7 @@ export default class Git {
return gitCommand(cwd, 'rev-parse', '--show-toplevel').then(data => data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/'));
}
static blame(format: GitBlameFormat, fileName: string, repoPath?: string, sha?: string) {
static blame(format: GitBlameFormat, fileName: string, sha?: string, repoPath?: string) {
const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath);
if (sha) {
@ -62,11 +62,20 @@ export default class Git {
return gitCommand(root, 'blame', format, '--root', '--', file);
}
static blameLines(format: GitBlameFormat, fileName: string, startLine: number, endLine: number, sha?: string, repoPath?: string) {
const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath);
if (sha) {
return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', `${sha}^`, '--', file);
}
return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', '--', file);
}
static getVersionedFile(fileName: string, repoPath: string, sha: string) {
return new Promise<string>((resolve, reject) => {
Git.getVersionedFileText(fileName, repoPath, sha).then(data => {
const ext = path.extname(fileName);
tmp.file({ prefix: `${path.basename(fileName, ext)}-${sha}_`, postfix: ext }, (err, destination, fd, cleanupCallback) => {
tmp.file({ prefix: `${path.basename(fileName, ext)}-${sha}__`, postfix: ext }, (err, destination, fd, cleanupCallback) => {
if (err) {
reject(err);
return;

+ 0
- 51
src/gitCodeActionProvider.ts View File

@ -1,51 +0,0 @@
'use strict';
import {CancellationToken, CodeActionContext, CodeActionProvider, Command, DocumentSelector, ExtensionContext, Range, TextDocument, Uri, window} from 'vscode';
import {Commands, DocumentSchemes} from './constants';
import GitProvider from './gitProvider';
import {DiagnosticSource} from './constants';
export default class GitCodeActionProvider implements CodeActionProvider {
static selector: DocumentSelector = { scheme: DocumentSchemes.File };
constructor(context: ExtensionContext, private git: GitProvider) { }
provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Command[] | Thenable<Command[]> {
if (!context.diagnostics.some(d => d.source === DiagnosticSource)) {
return [];
}
return this.git.getBlameForLine(document.fileName, range.start.line)
.then(blame => {
const actions: Command[] = [];
if (!blame) return actions;
if (blame.commit.sha) {
actions.push({
title: `GitLens: Diff ${blame.commit.sha} with working tree`,
command: Commands.DiffWithWorking,
arguments: [
Uri.file(document.fileName),
blame.commit.sha, blame.commit.uri,
blame.line.line
]
});
}
if (blame.commit.sha && blame.commit.previousSha) {
actions.push({
title: `GitLens: Diff ${blame.commit.sha} with previous ${blame.commit.previousSha}`,
command: Commands.DiffWithPrevious,
arguments: [
Uri.file(document.fileName),
blame.commit.repoPath,
blame.commit.sha, blame.commit.uri,
blame.commit.previousSha, blame.commit.previousUri,
blame.line.line
]
});
}
return actions;
});
}
}

+ 17
- 3
src/gitCodeLensProvider.ts View File

@ -158,7 +158,8 @@ export default class GitCodeLensProvider implements CodeLensProvider {
switch (this._config.recentChange.command) {
case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand<GitRecentChangeCodeLens>(title, lens, blame);
case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand<GitRecentChangeCodeLens>(title, lens, blame);
case CodeLensCommand.GitHistory: return this._applyGitHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame);
case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand<GitRecentChangeCodeLens>(title, lens, blame);
case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame);
default: return lens;
}
});
@ -171,7 +172,8 @@ export default class GitCodeLensProvider implements CodeLensProvider {
switch (this._config.authors.command) {
case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.GitHistory: return this._applyGitHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
default: return lens;
}
});
@ -195,12 +197,24 @@ export default class GitCodeLensProvider implements CodeLensProvider {
return lens;
}
_applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines) {
const line = blame.allLines[lens.range.start.line];
const commit = blame.commits.get(line.sha);
lens.command = {
title: title,
command: Commands.DiffWithPrevious,
arguments: [Uri.file(lens.fileName), commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line.line]
};
return lens;
}
_applyGitHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines) {
if (!this._hasGitHistoryExtension) return this._applyBlameExplorerCommand(title, lens, blame);
lens.command = {
title: title,
command: 'git.viewFileHistory',
command: CodeLensCommand.GitViewHistory,
arguments: [Uri.file(lens.fileName)]
};
return lens;

+ 39
- 15
src/gitProvider.ts View File

@ -27,6 +27,8 @@ enum RemoveCacheReason {
DocumentChanged
}
const UncommitedRegex = /^[0]+$/;
export default class GitProvider extends Disposable {
private _blameCache: Map<string, IBlameCacheEntry>|null;
private _blameCacheDisposable: Disposable|null;
@ -66,7 +68,7 @@ export default class GitProvider extends Disposable {
const subscriptions: Disposable[] = [];
subscriptions.push(workspace.onDidChangeConfiguration(() => this._onConfigure()));
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigure, this));
this._disposable = Disposable.from(...subscriptions);
}
@ -167,10 +169,8 @@ export default class GitProvider extends Disposable {
console.log('[GitLens]', `Skipping blame; ${fileName} is gitignored`);
blame = GitProvider.BlameEmptyPromise;
} else {
const enricher = new GitBlameParserEnricher(GitProvider.BlameFormat);
blame = Git.blame(GitProvider.BlameFormat, fileName)
.then(data => enricher.enrich(data, fileName))
.then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName))
.catch(ex => {
// Trap and cache expected blame errors
if (this.UseCaching) {
@ -198,18 +198,38 @@ export default class GitProvider extends Disposable {
});
}
getBlameForLine(fileName: string, line: number): Promise<IGitBlameLine|null> {
return this.getBlameForFile(fileName).then(blame => {
const blameLine = blame && blame.lines[line];
if (!blameLine) return null;
getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise<IGitBlameLine|null> {
if (this.UseCaching && !sha) {
return this.getBlameForFile(fileName).then(blame => {
const blameLine = blame && blame.lines[line];
if (!blameLine) return null;
const commit = blame.commits.get(blameLine.sha);
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
line: blameLine
};
});
}
const commit = blame.commits.get(blameLine.sha);
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
line: blameLine
};
});
fileName = Git.normalizePath(fileName);
return Git.blameLines(GitProvider.BlameFormat, fileName, line, line, sha, repoPath)
.then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName))
.then(blame => {
if (!blame) return null;
const commit = blame.commits.values().next().value;
if (repoPath) {
commit.repoPath = repoPath;
}
return <IGitBlameLine>{
author: blame.authors.values().next().value,
commit: commit,
line: blame.lines[line - 1]
};
});
}
getBlameForRange(fileName: string, range: Range): Promise<IGitBlameLines|null> {
@ -334,6 +354,10 @@ export default class GitProvider extends Disposable {
this._codeLensProviderDisposable = Disposable.from(...disposables);
}
static isUncommitted(sha: string) {
return UncommitedRegex.test(sha);
}
static fromBlameUri(uri: Uri): IGitBlameUriData {
if (uri.scheme !== DocumentSchemes.GitBlame) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`);
const data = GitProvider._fromGitUri<IGitBlameUriData>(uri);

Loading…
Cancel
Save