From dcb789f58d9fbb0e0c94b295279218ae2b4cb7d6 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 15 Sep 2016 04:26:49 -0400 Subject: [PATCH] Adds many new settings Adds new blame annotation styles (compact & expanded) Cleaned up blame annotations Fixes issue with invalid repoPath on first start --- README.md | 22 +++- package.json | 69 ++++++++++++- src/configuration.ts | 32 ++++++ src/extension.ts | 17 ++-- src/git.ts | 10 ++ src/gitBlameController.ts | 200 +++++++++++++++++++++++++++---------- src/gitCodeLensProvider.ts | 113 +++++++++++---------- src/gitProvider.ts | 11 +- 8 files changed, 359 insertions(+), 115 deletions(-) create mode 100644 src/configuration.ts diff --git a/README.md b/README.md index a8f98b9..eda49b8 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Provides Git blame and blame history CodeLens for many supported Visual Studio C ## Features -Provides two CodeLens on code blocks : +Provides two CodeLens on code blocks: - **Recent Change** - author and date of the most recent check-in > Clicking on the CodeLens opens a **Blame explorer** with the commits and changed lines in the right pane and the commit (file) contents on the left -- **Blame** - number of authors of a block and the most prominent author (if there are more than one) - > Clicking on the CodeLens toggles Git blame overlay +- **Authors** - number of authors of a block and the most prominent author (if there are more than one) + > Clicking on the CodeLens toggles Git blame annotations on/off +## Screenshot > ![GitLens preview](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/preview-gitlens.gif) ## Requirements @@ -18,7 +19,7 @@ Must be using Git and it must be in your path. ## Extension Settings -None yet. +See the Contributions tab above ## Known Issues @@ -29,7 +30,20 @@ None yet. ## Release Notes +### 0.1.0 + + - Improved blame annotations, now with sha and author by default + - Add new blame annotation styles -- compact and expanded (default) + - Adds many new configuration settings; see Contributions tab above + +### 0.0.7 + + - Fixes [#4](https://github.com/eamodio/vscode-gitlens/issues/4) - Absolute paths fail on Windows due to backslash (Really!) + - Fixes [#5](https://github.com/eamodio/vscode-gitlens/issues/5) - Finding first non-white-space fails sometimes + - Adds .gitignore checks to reduce the number of blame calls + ### 0.0.6 + - Fixes [#2](https://github.com/eamodio/vscode-gitlens/issues/2) - [request] Provide some debug info when things fail - Fixes [#4](https://github.com/eamodio/vscode-gitlens/issues/4) - Absolute paths fail on Windows due to backslash - Attempts to scroll to the correct position when opening a diff diff --git a/package.json b/package.json index 5660c30..c359eb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlens", - "version": "0.0.7", + "version": "0.1.0", "author": "Eric Amodio", "publisher": "eamodio", "engines": { @@ -19,7 +19,7 @@ "color": "#0000FF", "theme": "dark" }, - "preview": true, + "preview": false, "bugs": { "url": "https://github.com/eamodio/vscode-gitlens/issues" }, @@ -29,6 +29,71 @@ }, "main": "./out/src/extension", "contributes": { + "configuration": { + "type": "object", + "title": "GitLens configuration", + "properties": { + "gitlens.blame.annotation.style": { + "type": "string", + "default": "expanded", + "enum": [ + "compact", + "expanded" + ], + "description": "Specifies the style of the blame annotations. Compact - groups annotations to limit the repetition and also adds author and date when possible. Expanded - shows an annotation on every line" + }, + "gitlens.blame.annotation.sha": { + "type": "boolean", + "default": true, + "description": "Specifies whether the commit sha will be shown in the blame annotations. Applies only to the Expanded annotation style" + }, + "gitlens.blame.annotation.author": { + "type": "boolean", + "default": true, + "description": "Specifies whether the committer will be shown in the blame annotations. Applies only to the Expanded annotation style" + }, + "gitlens.blame.annotation.date": { + "type": "boolean", + "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.recentChange.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether the recent change CodeLens is shown" + }, + "gitlens.codeLens.recentChange.command": { + "type": "string", + "default": "blame.explorer", + "enum": [ + "blame.annotate", + "blame.explorer", + "git.history" + ], + "description": "Specifies the command executed when the recent change CodeLens is clicked. Annotate - toggles blame annotations. Explorer - opens the blame explorer. History - opens a file history picker, which requires the Git History (git log) extension" + }, + "gitlens.codeLens.authors.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether the authors CodeLens is shown" + }, + "gitlens.codeLens.authors.command": { + "type": "string", + "default": "blame.annotate", + "enum": [ + "blame.annotate", + "blame.explorer", + "git.history" + ], + "description": "Specifies the command executed when the authors CodeLens is clicked. Annotate - toggles blame annotations. Explorer - opens the blame explorer. History - opens a file history picker, which requires the Git History (git log) extension" + } + } + }, "commands": [{ "command": "gitlens.diffWithPrevious", "title": "Git: Open Diff with Previous Commit", diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 0000000..9523591 --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,32 @@ +export type BlameAnnotationStyle = 'compact' | 'expanded'; +export const BlameAnnotationStyle = { + Compact: 'compact' as BlameAnnotationStyle, + Expanded: 'expanded' as BlameAnnotationStyle +} + +export interface IBlameConfig { + annotation: { + style: BlameAnnotationStyle; + sha: boolean; + author: boolean; + date: boolean; + useCodeActions: boolean; + }; +} + +export type CodeLensCommand = 'blame.annotate' | 'blame.explorer' | 'git.history'; +export const CodeLensCommand = { + BlameAnnotate: 'blame.annotate' as CodeLensCommand, + BlameExplorer: 'blame.explorer' as CodeLensCommand, + GitHistory: 'git.history' as CodeLensCommand +} + +export interface ICodeLensConfig { + enabled: boolean; + command: CodeLensCommand; +} + +export interface ICodeLensesConfig { + recentChange: ICodeLensConfig; + authors: ICodeLensConfig; +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 9aaa6ec..b617748 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,9 @@ import GitBlameCodeLensProvider from './gitBlameCodeLensProvider'; import GitBlameContentProvider from './gitBlameContentProvider'; import GitBlameController from './gitBlameController'; import GitProvider from './gitProvider'; +import Git from './git'; import {DiffWithPreviousCommand, DiffWithWorkingCommand, ShowBlameCommand, ShowBlameHistoryCommand, ToggleBlameCommand} from './commands'; +import {ICodeLensesConfig} from './configuration'; import {WorkspaceState} from './constants'; // this method is called when your extension is activated @@ -19,17 +21,20 @@ export function activate(context: ExtensionContext) { console.log(`GitLens active: ${workspace.rootPath}`); - const git = new GitProvider(context); - context.subscriptions.push(git); - - git.getRepoPath(workspace.rootPath).then(repoPath => { + Git.repoPath(workspace.rootPath).then(repoPath => { context.workspaceState.update(WorkspaceState.RepoPath, repoPath); - //context.workspaceState.update(WorkspaceState.HasGitHistoryExtension, extensions.getExtension('donjayamanne.githistory') !== undefined); + context.workspaceState.update(WorkspaceState.HasGitHistoryExtension, extensions.getExtension('donjayamanne.githistory') !== undefined); + + const git = new GitProvider(context); + context.subscriptions.push(git); context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git))); - context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); + const config = workspace.getConfiguration('gitlens').get('codeLens'); + if (config.recentChange.enabled || config.authors.enabled) { + context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); + } const blameController = new GitBlameController(context, git); context.subscriptions.push(blameController); diff --git a/src/git.ts b/src/git.ts index b06e60b..2aaf5bd 100644 --- a/src/git.ts +++ b/src/git.ts @@ -53,6 +53,16 @@ export default class Git { return gitCommand(repoPath, 'blame', '--porcelain', '--root', '--', fileName); } + static blameLinePorcelain(fileName: string, repoPath: string, sha?: string) { + fileName = Git.normalizePath(fileName, repoPath); + + if (sha) { + return gitCommand(repoPath, 'blame', '--line-porcelain', '--root', `${sha}^`, '--', fileName); + } + + return gitCommand(repoPath, 'blame', '--line-porcelain', '--root', '--', fileName); + } + static getVersionedFile(fileName: string, repoPath: string, sha: string) { return new Promise((resolve, reject) => { Git.getVersionedFileText(fileName, repoPath, sha).then(data => { diff --git a/src/gitBlameController.ts b/src/gitBlameController.ts index 2d96f07..6478a4b 100644 --- a/src/gitBlameController.ts +++ b/src/gitBlameController.ts @@ -1,18 +1,17 @@ 'use strict' import {commands, DecorationInstanceRenderOptions, DecorationOptions, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, ExtensionContext, languages, OverviewRulerLane, Position, Range, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode'; import {BuiltInCommands, Commands, DocumentSchemes} from './constants'; -import GitProvider, {IGitBlame} from './gitProvider'; +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', - width: '5em' - }, + margin: '0 1.75em 0 0' + } }); - let highlightDecoration: TextEditorDecorationType; export default class GitBlameController extends Disposable { @@ -92,9 +91,11 @@ export default class GitBlameController extends Disposable { class GitBlameEditorController extends Disposable { public uri: Uri; - private _disposable: Disposable; + private _blame: Promise; + private _config: IBlameConfig; private _diagnostics: DiagnosticCollection; + private _disposable: Disposable; private _toggleWhitespace: boolean; constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) { @@ -104,24 +105,28 @@ class GitBlameEditorController extends Disposable { const fileName = this.uri.fsPath; this._blame = this.git.getBlameForFile(fileName); + this._config = workspace.getConfiguration('gitlens').get('blame'); + const subscriptions: Disposable[] = []; - this._diagnostics = languages.createDiagnosticCollection(DiagnosticCollectionName); - subscriptions.push(this._diagnostics); + 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(languages.registerCodeActionsProvider(GitCodeActionsProvider.selector, new GitCodeActionsProvider(this.context, this.git))); + } subscriptions.push(window.onDidChangeTextEditorSelection(e => { const activeLine = e.selections[0].active.line; - this._diagnostics.clear(); + 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.set(editor.document.uri, [this._getDiagnostic(editor, activeLine, blame.commit.sha)]); + this._diagnostics && this._diagnostics.set(editor.document.uri, [this._getDiagnostic(editor, activeLine, blame.commit.sha)]); this.applyHighlight(blame.commit.sha); }); @@ -156,60 +161,151 @@ class GitBlameEditorController extends Disposable { if (!blame || !blame.lines.length) return; // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off - this._toggleWhitespace = workspace.getConfiguration('editor').get('renderWhitespace') as boolean; + const whitespace = workspace.getConfiguration('editor').get('renderWhitespace'); + this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none'; if (this._toggleWhitespace) { commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); } - let lastSha; - const blameDecorationOptions: DecorationOptions[] = blame.lines.map(l => { - let color = '#6b6b6b'; - - const c = blame.commits.get(l.sha); - if (c.previousSha) { - color = '#999999'; - } - - let gutter = ''; - if (lastSha !== l.sha || true) { // TODO: Add a config option - gutter = l.sha.substring(0, 8); - if (gutter === '00000000') { - if (c.previousSha) { - const pc = blame.commits.get(c.previousSha); - if (pc && pc.lines.find(_ => _.line === l.line)) { - gutter = c.previousSha.substring(0, 8); - color = 'rgba(0, 188, 242, 0.6)'; - } - else { - color = 'rgba(127, 186, 0, 0.6)'; - } - } else { - color = 'rgba(127, 186, 0, 0.6)'; - } - } - } - lastSha = l.sha; - - return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), - hoverMessage: `${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, - renderOptions: { before: { color: color, contentText: gutter } } - }; - }); - + let blameDecorationOptions: DecorationOptions[] + switch (this._config.annotation.style) { + case BlameAnnotationStyle.Compact: + blameDecorationOptions = this._getCompactGutterDecorations(blame); + break; + case BlameAnnotationStyle.Expanded: + blameDecorationOptions = this._getExpandedGutterDecorations(blame); + break; + } this.editor.setDecorations(blameDecoration, blameDecorationOptions); sha = sha || blame.commits.values().next().value.sha; - // 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)]); + 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++; + if (count === 1) { + gutter = `\\00a6\\00a0 ${this._getAuthor(commit, 17, true)}`; + } else if (count === 2) { + gutter = `\\00a6\\00a0 ${this._getDate(commit, true)}`; + } else { + gutter = '\\00a6\\00a0'; + } + } else { + count = 0; + gutter = commit.sha.substring(0, 8); + } + lastSha = l.sha; + + return { + range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + hoverMessage: [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 = ''; + // if (l.previousSha) { + // let previousCommit = blame.commits.get(l.previousSha); + // if (previousCommit) {//} && previousCommit.lines.find(_ => _.line === l.originalLine)) { + // commit = previousCommit; + // color = 'rgba(0, 188, 242, 0.6)'; + // } + // else { + // color = 'rgba(127, 186, 0, 0.6)'; + // } + // } else { + // color = 'rgba(127, 186, 0, 0.6)'; + // } + } + + 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; diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 1c5af75..f3f236d 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,6 +1,7 @@ 'use strict'; -import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window} from 'vscode'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window, workspace} from 'vscode'; import {BuiltInCommands, Commands, DocumentSchemes, WorkspaceState} from './constants'; +import {CodeLensCommand, ICodeLensesConfig} from './configuration'; import GitProvider, {IGitBlame, IGitBlameLines, IGitCommit} from './gitProvider'; import * as moment from 'moment'; import * as _ from 'lodash'; @@ -15,7 +16,7 @@ export class GitRecentChangeCodeLens extends CodeLens { } } -export class GitBlameCodeLens extends CodeLens { +export class GitAuthorsCodeLens extends CodeLens { constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, range: Range) { super(range); } @@ -25,20 +26,16 @@ export class GitBlameCodeLens extends CodeLens { } } -// export class GitHistoryCodeLens extends CodeLens { -// constructor(public repoPath: string, public fileName: string, range: Range) { -// super(range); -// } -// } - export default class GitCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.File }; - // private hasGitHistoryExtension: boolean; + private _config: ICodeLensesConfig; + private _hasGitHistoryExtension: boolean; constructor(context: ExtensionContext, private git: GitProvider) { - // this.hasGitHistoryExtension = context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false); - } + this._config = workspace.getConfiguration('gitlens').get('codeLens'); + this._hasGitHistoryExtension = context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false); + } provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { const fileName = document.fileName; @@ -55,11 +52,12 @@ export default class GitCodeLensProvider implements CodeLensProvider { // Check if we have a lens for the whole document -- if not add one if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); - lenses.push(new GitRecentChangeCodeLens(this.git, fileName, SymbolKind.File, blameRange, new Range(0, 0, 0, blameRange.start.character))); - lenses.push(new GitBlameCodeLens(this.git, fileName, SymbolKind.File, blameRange, new Range(0, 1, 0, blameRange.start.character))); - // if (this.hasGitHistoryExtension) { - // lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, new Range(0, 1, 0, blameRange.start.character))); - // } + if (this._config.recentChange.enabled) { + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, SymbolKind.File, blameRange, new Range(0, 0, 0, blameRange.start.character))); + } + if (this._config.authors.enabled) { + lenses.push(new GitAuthorsCodeLens(this.git, fileName, SymbolKind.File, blameRange, new Range(0, 1, 0, blameRange.start.character))); + } } return lenses; @@ -107,57 +105,72 @@ export default class GitCodeLensProvider implements CodeLensProvider { startChar += Math.floor(symbol.name.length / 2); } - lenses.push(new GitRecentChangeCodeLens(this.git, fileName, symbol.kind, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); - startChar++; - if (multiline) { - lenses.push(new GitBlameCodeLens(this.git, fileName, symbol.kind, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + if (this._config.recentChange.enabled) { + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, symbol.kind, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); startChar++; } - // if (this.hasGitHistoryExtension) { - // lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, line.range.with(new Position(line.range.start.line, startChar)))); - // } + if (multiline && this._config.authors.enabled) { + lenses.push(new GitAuthorsCodeLens(this.git, fileName, symbol.kind, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + } } resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token); - if (lens instanceof GitBlameCodeLens) return this._resolveGitBlameCodeLens(lens, token); - // if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token); + if (lens instanceof GitAuthorsCodeLens) return this._resolveGitAuthorsCodeLens(lens, token); } _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable { return lens.getBlame().then(blame => { const recentCommit = blame.commits.values().next().value; - lens.command = { - title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - ${SymbolKind[lens.symbolKind]}(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`, - command: Commands.ShowBlameHistory, - arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] - }; - return lens; + const title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; // - ${SymbolKind[lens.symbolKind]}(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`; + 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); + default: return lens; + } }); } - _resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable { + _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): Thenable { return lens.getBlame().then(blame => { - const editor = window.activeTextEditor; - const activeLine = editor.selection.active.line; - const count = blame.authors.size; - lens.command = { - title: `${count} ${count > 1 ? 'authors' : 'author'} (${blame.authors.values().next().value.name}${count > 1 ? ' and others' : ''})`, - command: Commands.ToggleBlame, - arguments: [Uri.file(lens.fileName), blame.allLines[activeLine].sha] - }; - return lens; + const title = `${count} ${count > 1 ? 'authors' : 'author'} (${blame.authors.values().next().value.name}${count > 1 ? ' and others' : ''})`; + 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); + default: return lens; + } }); } - // _resolveGitHistoryCodeLens(lens: GitHistoryCodeLens, token: CancellationToken): Thenable { - // // TODO: Play with this more -- get this to open the correct diff to the right place - // lens.command = { - // title: `View History`, - // command: 'git.viewFileHistory', // viewLineHistory - // arguments: [Uri.file(lens.fileName)] - // }; - // return Promise.resolve(lens); - // } + _applyBlameAnnotateCommand(title: string, lens: T, blame: IGitBlameLines, sha?: string) { + lens.command = { + title: title, + command: Commands.ToggleBlame, + arguments: [Uri.file(lens.fileName), sha] + }; + return lens; + } + + _applyBlameExplorerCommand(title: string, lens: T, blame: IGitBlameLines) { + lens.command = { + title: title, + command: Commands.ShowBlameHistory, + arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] + }; + 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', + arguments: [Uri.file(lens.fileName)] + }; + return lens; + } } \ No newline at end of file diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 0161f6a..bf430a6 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -11,6 +11,7 @@ import * as ignore from 'ignore'; const commitMessageMatcher = /^([\^0-9a-fA-F]{7})\s(.*)$/gm; const blamePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n(?:^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n)?^(.*)$/gm; +const blameLinePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n^(.*)$/gm; interface IBlameCacheEntry { //date: Date; @@ -70,6 +71,7 @@ export default class GitProvider extends Disposable { subscriptions.push(workspace.onDidCloseTextDocument(d => this._removeCachedBlame(d.fileName, RemoveCacheReason.DocumentClosed))); subscriptions.push(workspace.onDidSaveTextDocument(d => this._removeCachedBlameFn(d.fileName, RemoveCacheReason.DocumentSaved))); subscriptions.push(workspace.onDidChangeTextDocument(e => this._removeCachedBlameFn(e.document.fileName, RemoveCacheReason.DocumentChanged))); + subscriptions.push(workspace.onDidChangeConfiguration(() => this._registerCodeLensProvider())); this._disposable = Disposable.from(...subscriptions); } @@ -128,6 +130,7 @@ export default class GitProvider extends Disposable { console.log('[GitLens]', `Skipping blame; ${fileName} is gitignored`); blame = GitProvider.BlameEmptyPromise; } else { + //blame = Git.blameLinePorcelain(fileName, this.repoPath) blame = Git.blamePorcelain(fileName, this.repoPath) .then(data => { if (!data) return null; @@ -137,8 +140,10 @@ export default class GitProvider extends Disposable { const lines: Array = []; let m: Array; + //while ((m = blameLinePorcelainMatcher.exec(data)) != null) { while ((m = blamePorcelainMatcher.exec(data)) != null) { const sha = m[1].substring(0, 8); + const previousSha = m[14]; let commit = commits.get(sha); if (!commit) { const authorName = m[5].trim(); @@ -158,7 +163,6 @@ export default class GitProvider extends Disposable { commit.originalFileName = originalFileName; } - const previousSha = m[14]; if (previousSha) { commit.previousSha = previousSha.substring(0, 8); commit.previousFileName = m[15]; @@ -174,6 +178,10 @@ export default class GitProvider extends Disposable { //code: m[17] } + if (previousSha) { + line.previousSha = previousSha.substring(0, 8); + } + commit.lines.push(line); lines.push(line); } @@ -485,6 +493,7 @@ class GitCommit implements IGitCommit { export interface IGitCommitLine { sha: string; + previousSha?: string; line: number; originalLine: number; code?: string;