From 834b4904db00bb132d721c0c457ef3c2554211f5 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 21 Sep 2016 02:02:49 -0400 Subject: [PATCH] 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 --- README.md | 16 +- package.json | 53 ++++--- src/blameAnnotationController.ts | 286 ++++++++++++++++++++++++++++++++++ src/blameStatusBarController.ts | 111 ++++++++++++++ src/commands.ts | 94 +++++++----- src/configuration.ts | 26 +++- src/constants.ts | 6 +- src/extension.ts | 21 +-- src/git/git.ts | 13 +- src/gitBlameController.ts | 322 --------------------------------------- src/gitCodeActionProvider.ts | 51 ------- src/gitCodeLensProvider.ts | 20 ++- src/gitProvider.ts | 54 +++++-- 13 files changed, 606 insertions(+), 467 deletions(-) create mode 100644 src/blameAnnotationController.ts create mode 100644 src/blameStatusBarController.ts delete mode 100644 src/gitBlameController.ts delete mode 100644 src/gitCodeActionProvider.ts diff --git a/README.md b/README.md index 4d8053c..a388f42 100644 --- a/README.md +++ b/README.md @@ -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!) diff --git a/package.json b/package.json index 0378d8a..72309a0 100644 --- a/package.json +++ b/package.json @@ -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" } } \ No newline at end of file diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts new file mode 100644 index 0000000..c0de3ee --- /dev/null +++ b/src/blameAnnotationController.ts @@ -0,0 +1,286 @@ +'use strict' +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 * as moment from 'moment'; + +const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ + before: { + margin: '0 1.75em 0 0' + } +}); +let highlightDecoration: TextEditorDecorationType; + +export default class BlameAnnotationController extends Disposable { + private _disposable: Disposable; + private _editorController: EditorBlameAnnotationController|null; + + constructor(private context: ExtensionContext, private git: GitProvider) { + super(() => this.dispose()); + + if (!highlightDecoration) { + highlightDecoration = window.createTextEditorDecorationType({ + dark: { + backgroundColor: 'rgba(255, 255, 255, 0.15)', + gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), + overviewRulerColor: 'rgba(255, 255, 255, 0.75)', + }, + light: { + backgroundColor: 'rgba(0, 0, 0, 0.15)', + gutterIconPath: context.asAbsolutePath('images/blame-light.png'), + overviewRulerColor: 'rgba(0, 0, 0, 0.75)', + }, + gutterIconSize: 'contain', + overviewRulerLane: OverviewRulerLane.Right, + isWholeLine: true + }); + } + + const subscriptions: Disposable[] = []; + + // subscriptions.push(window.onDidChangeActiveTextEditor(e => { + // if (!e || !this._controller || this._controller.editor === e) return; + // this.clear(); + // })); + + workspace.onDidCloseTextDocument(d => { + if (!this._editorController || this._editorController.uri.toString() !== d.uri.toString()) return; + this.clear(); + }) + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + this.clear(); + this._disposable && this._disposable.dispose(); + } + + clear() { + this._editorController && this._editorController.dispose(); + this._editorController = null; + } + + showBlameAnnotation(editor: TextEditor, sha?: string) { + if (!editor || !editor.document || editor.document.isUntitled) { + this.clear(); + return; + } + + if (!this._editorController) { + this._editorController = new EditorBlameAnnotationController(this.context, this.git, editor); + return this._editorController.applyBlameAnnotation(sha); + } + } + + toggleBlameAnnotation(editor: TextEditor, sha?: string) { + if (!editor ||!editor.document || editor.document.isUntitled || this._editorController) { + this.clear(); + return; + } + + return this.showBlameAnnotation(editor, sha); + } +} + +class EditorBlameAnnotationController extends Disposable { + public uri: Uri; + + private _blame: Promise; + private _config: IBlameConfig; + private _disposable: Disposable; + private _document: TextDocument; + private _toggleWhitespace: boolean; + + constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) { + super(() => this.dispose()); + + this._document = this.editor.document; + this.uri = this._document.uri; + + this._blame = this.git.getBlameForFile(this.uri.fsPath); + + this._config = workspace.getConfiguration('gitlens').get('blame'); + + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + if (this.editor) { + // HACK: This only works when switching to another editor - diffs handle whitespace toggle differently + if (this._toggleWhitespace) { + commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); + } + + this.editor.setDecorations(blameDecoration, []); + this.editor.setDecorations(highlightDecoration, []); + } + + this._disposable && this._disposable.dispose(); + } + + private _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) { + this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line) + .then(blame => blame && this.applyHighlight(blame.commit.sha)); + } + + applyBlameAnnotation(sha?: string) { + return this._blame.then(blame => { + if (!blame || !blame.lines.length) return; + + // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off + const whitespace = workspace.getConfiguration('editor').get('renderWhitespace'); + this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none'; + if (this._toggleWhitespace) { + commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); + } + + let blameDecorationOptions: DecorationOptions[] | undefined; + switch (this._config.annotation.style) { + case BlameAnnotationStyle.Compact: + blameDecorationOptions = this._getCompactGutterDecorations(blame); + break; + case BlameAnnotationStyle.Expanded: + blameDecorationOptions = this._getExpandedGutterDecorations(blame); + break; + } + + if (blameDecorationOptions) { + this.editor.setDecorations(blameDecoration, blameDecorationOptions); + } + + sha = sha || blame.commits.values().next().value.sha; + + return this.applyHighlight(sha); + }); + } + + 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 = [`_${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)'; + hoverMessage = ''; + } + + let gutter = ''; + if (lastSha !== l.sha) { + count = -1; + } + + const isEmptyOrWhitespace = this._document.lineAt(l.line).isEmptyOrWhitespace; + if (!isEmptyOrWhitespace) { + switch (++count) { + case 0: + gutter = commit.sha.substring(0, 8); + break; + case 1: + gutter = `\\00a6\\00a0 ${this._getAuthor(commit, 17, true)}`; + break; + case 2: + gutter = `\\00a6\\00a0 ${this._getDate(commit, true)}`; + break; + default: + gutter = '\\00a6\\00a0'; + break; + } + } + + lastSha = l.sha; + + return { + range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + hoverMessage: [`_${l.sha}_: ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`], + renderOptions: { before: { color: color, contentText: gutter, width: '11em' } } + }; + }); + } + + private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] { + let width = 0; + if (this._config.annotation.sha) { + width += 5; + } + if (this._config.annotation.date) { + if (width > 0) { + width += 7; + } else { + width += 6; + } + } + if (this._config.annotation.author) { + if (width > 5 + 6) { + width += 12; + } else if (width > 0) { + width += 11; + } else { + width += 10; + } + } + + return blame.lines.map(l => { + let color = l.previousSha ? '#999999' : '#6b6b6b'; + let commit = blame.commits.get(l.sha); + let hoverMessage: string | Array = [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)'; + hoverMessage = ''; + } + + const gutter = this._getGutter(commit); + return { + range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + hoverMessage: hoverMessage, + renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } } + }; + }); + } + + 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`; + } + return commit.author; + } + + private _getDate(commit: IGitCommit, force?: boolean) { + if (!force && !this._config.annotation.date) return ''; + return moment(commit.date).format('MM/DD/YYYY'); + } + + private _getGutter(commit: IGitCommit) { + const author = this._getAuthor(commit); + const date = this._getDate(commit); + if (this._config.annotation.sha) { + return `${commit.sha.substring(0, 8)}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; + } else if (this._config.annotation.date) { + return `${date}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; + } else { + return author; + } + } +} \ No newline at end of file diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts new file mode 100644 index 0000000..020a4cc --- /dev/null +++ b/src/blameStatusBarController.ts @@ -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('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(); + } +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 8483b4a..1b017ac 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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)); } } diff --git a/src/configuration.ts b/src/configuration.ts index 43c79b5..0bc5b79 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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 } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index cbddf2e..2045566 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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, } diff --git a/src/extension.ts b/src/extension.ts index dba7c17..a6e3973 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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() { -} \ No newline at end of file +export function deactivate() { } \ No newline at end of file diff --git a/src/git/git.ts b/src/git/git.ts index d024522..9b6e003 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -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((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; diff --git a/src/gitBlameController.ts b/src/gitBlameController.ts deleted file mode 100644 index 4102514..0000000 --- a/src/gitBlameController.ts +++ /dev/null @@ -1,322 +0,0 @@ -'use strict' -import {commands, DecorationInstanceRenderOptions, DecorationOptions, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, ExtensionContext, languages, OverviewRulerLane, Position, Range, TextDocument, TextEditor, TextEditorDecorationType, 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({ - before: { - margin: '0 1.75em 0 0' - } -}); -let highlightDecoration: TextEditorDecorationType; - -export default class GitBlameController extends Disposable { - private _controller: GitBlameEditorController|null; - private _disposable: Disposable; - - private _blameDecoration: TextEditorDecorationType; - private _highlightDecoration: TextEditorDecorationType; - - constructor(private context: ExtensionContext, private git: GitProvider) { - super(() => this.dispose()); - - if (!highlightDecoration) { - highlightDecoration = window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(255, 255, 255, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), - overviewRulerColor: 'rgba(255, 255, 255, 0.75)', - }, - light: { - backgroundColor: 'rgba(0, 0, 0, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-light.png'), - overviewRulerColor: 'rgba(0, 0, 0, 0.75)', - }, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Right, - isWholeLine: true - }); - } - - const subscriptions: Disposable[] = []; - - // subscriptions.push(window.onDidChangeActiveTextEditor(e => { - // if (!e || !this._controller || this._controller.editor === e) return; - // this.clear(); - // })); - - workspace.onDidCloseTextDocument(d => { - if (!this._controller || this._controller.uri.toString() !== d.uri.toString()) return; - this.clear(); - }) - - this._disposable = Disposable.from(...subscriptions); - } - - dispose() { - this.clear(); - this._disposable && this._disposable.dispose(); - } - - clear() { - this._controller && this._controller.dispose(); - this._controller = null; - } - - showBlame(editor: TextEditor, sha?: string) { - if (!editor) { - this.clear(); - return; - } - - if (!this._controller) { - this._controller = new GitBlameEditorController(this.context, this.git, editor); - return this._controller.applyBlame(sha); - } - } - - toggleBlame(editor: TextEditor, sha?: string) { - if (!editor || this._controller) { - this.clear(); - return; - } - - return this.showBlame(editor, sha); - } -} - -class GitBlameEditorController extends Disposable { - public uri: Uri; - - private _blame: Promise; - private _config: IBlameConfig; - private _diagnostics: DiagnosticCollection; - private _disposable: Disposable; - private _document: TextDocument; - private _toggleWhitespace: boolean; - - constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) { - super(() => this.dispose()); - - this._document = this.editor.document; - this.uri = this._document.uri; - const fileName = this.uri.fsPath; - - this._blame = this.git.getBlameForFile(fileName); - - this._config = workspace.getConfiguration('gitlens').get('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); - }); - })); - - this._disposable = Disposable.from(...subscriptions); - } - - dispose() { - if (this.editor) { - // HACK: This only works when switching to another editor - diffs handle whitespace toggle differently - if (this._toggleWhitespace) { - commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); - } - - this.editor.setDecorations(blameDecoration, []); - this.editor.setDecorations(highlightDecoration, []); - } - - 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; - } - - applyBlame(sha?: string) { - return this._blame.then(blame => { - if (!blame || !blame.lines.length) return; - - // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off - const whitespace = workspace.getConfiguration('editor').get('renderWhitespace'); - this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none'; - if (this._toggleWhitespace) { - commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); - } - - let blameDecorationOptions: DecorationOptions[] | undefined; - switch (this._config.annotation.style) { - case BlameAnnotationStyle.Compact: - blameDecorationOptions = this._getCompactGutterDecorations(blame); - break; - case BlameAnnotationStyle.Expanded: - blameDecorationOptions = this._getExpandedGutterDecorations(blame); - break; - } - - if (blameDecorationOptions) { - this.editor.setDecorations(blameDecoration, blameDecorationOptions); - } - - 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[] { - 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 = [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)'; - hoverMessage = ''; - } - - let gutter = ''; - if (lastSha !== l.sha) { - count = -1; - } - - const isEmptyOrWhitespace = this._document.lineAt(l.line).isEmptyOrWhitespace; - if (!isEmptyOrWhitespace) { - switch (++count) { - case 0: - gutter = commit.sha.substring(0, 8); - break; - case 1: - gutter = `\\00a6\\00a0 ${this._getAuthor(commit, 17, true)}`; - break; - case 2: - gutter = `\\00a6\\00a0 ${this._getDate(commit, true)}`; - break; - default: - gutter = '\\00a6\\00a0'; - break; - } - } - - lastSha = l.sha; - - return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), - hoverMessage: [`_${l.sha}_: ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`], - renderOptions: { before: { color: color, contentText: gutter, width: '11em' } } - }; - }); - } - - _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] { - let width = 0; - if (this._config.annotation.sha) { - width += 5; - } - if (this._config.annotation.date) { - if (width > 0) { - width += 7; - } else { - width += 6; - } - } - if (this._config.annotation.author) { - if (width > 5 + 6) { - width += 12; - } else if (width > 0) { - width += 11; - } else { - width += 10; - } - } - - return blame.lines.map(l => { - let color = l.previousSha ? '#999999' : '#6b6b6b'; - let commit = blame.commits.get(l.sha); - let hoverMessage: string | Array = [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)'; - hoverMessage = ''; - } - - const gutter = this._getGutter(commit); - return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), - hoverMessage: hoverMessage, - renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } } - }; - }); - } - - _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`; - } - return commit.author; - } - - _getDate(commit: IGitCommit, force?: boolean) { - if (!force && !this._config.annotation.date) return ''; - return moment(commit.date).format('MM/DD/YYYY'); - } - - _getGutter(commit: IGitCommit) { - const author = this._getAuthor(commit); - const date = this._getDate(commit); - if (this._config.annotation.sha) { - return `${commit.sha.substring(0, 8)}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; - } else if (this._config.annotation.date) { - return `${date}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; - } else { - 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); - }); - } -} \ No newline at end of file diff --git a/src/gitCodeActionProvider.ts b/src/gitCodeActionProvider.ts deleted file mode 100644 index 08764e3..0000000 --- a/src/gitCodeActionProvider.ts +++ /dev/null @@ -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 { - 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; - }); - } -} \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index c894d0e..c40465f 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -158,7 +158,8 @@ export default class GitCodeLensProvider implements CodeLensProvider { switch (this._config.recentChange.command) { case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand(title, lens, blame); case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand(title, lens, blame); - case CodeLensCommand.GitHistory: return this._applyGitHistoryCommand(title, lens, blame); + case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); + case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(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(title, lens, blame); case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand(title, lens, blame); - case CodeLensCommand.GitHistory: return this._applyGitHistoryCommand(title, lens, blame); + case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); + case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); default: return lens; } }); @@ -195,12 +197,24 @@ export default class GitCodeLensProvider implements CodeLensProvider { return lens; } + _applyDiffWithPreviousCommand(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(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; diff --git a/src/gitProvider.ts b/src/gitProvider.ts index f26ec07..9d95b97 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -27,6 +27,8 @@ enum RemoveCacheReason { DocumentChanged } +const UncommitedRegex = /^[0]+$/; + export default class GitProvider extends Disposable { private _blameCache: Map|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 { - 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 { + 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 { + author: blame.authors.values().next().value, + commit: commit, + line: blame.lines[line - 1] + }; + }); } getBlameForRange(fileName: string, range: Range): Promise { @@ -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(uri);