From f4410be30af9ceea22a8a80eed4c898ffaed9523 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 10 Nov 2016 03:22:43 -0500 Subject: [PATCH] Adds error messages for failed operations Adds showHistory command support to CodeLens Fixes and improve the showHistory explorer Refactoring --- CHANGELOG.md | 18 +- README.md | 16 +- package.json | 129 +++++++++++--- src/blameAnnotationController.ts | 4 +- src/blameAnnotationProvider.ts | 8 +- src/commands/diffLineWithPrevious.ts | 52 ++++++ src/commands/diffLineWithWorking.ts | 42 +++++ src/commands/diffWithPrevious.ts | 90 +++++----- src/commands/diffWithWorking.ts | 60 +++---- src/commands/showBlame.ts | 5 +- src/commands/showBlameHistory.ts | 5 +- src/commands/showHistory.ts | 9 +- src/commands/toggleBlame.ts | 5 +- src/configuration.ts | 17 +- src/constants.ts | 4 +- src/extension.ts | 8 +- src/git/enrichers/blameParserEnricher.ts | 4 +- src/git/enrichers/logParserEnricher.ts | 20 ++- src/git/git.ts | 8 +- src/git/gitEnrichment.ts | 28 ++- src/gitBlameCodeLensProvider.ts | 12 +- src/gitCodeLensProvider.ts | 209 ++++++++++++++--------- src/gitProvider.ts | 124 ++++++++------ src/logger.ts | 6 +- src/system/iterable.ts | 6 + 25 files changed, 584 insertions(+), 305 deletions(-) create mode 100644 src/commands/diffLineWithPrevious.ts create mode 100644 src/commands/diffLineWithWorking.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7863be5..a936117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,20 @@ --- ## Release Notes -### 1.0.0 +### 0.9.0 - - Adds support for git history (log) - - Adds `gitlens.diffWithPrevious` command to the explore context menu + - Adds support for git history (log)! + - Adds new `gitlens.showHistory` command to open the history explorer + - Adds new `gitlens.showHistory` option to the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings + - Adds per-language CodeLens location customization using the `gitlens.codeLens.languageLocations` setting + - Adds new `gitlens.diffLineWithPrevious` command for line sensitive diffs + - Adds new `gitlens.diffLineWithWorking` command for line sensitive diffs + - Adds `gitlens.diffWithPrevious` command to the explorer context menu - Adds output channel logging, controlled by the `gitlens.advanced.output.level` setting - - Changes `gitlens.diffWithPrevious` command to only be line sensitive if blame annotations are visible, otherwise it uses file history - - Changes `gitlens.diffWithWorking` command to only be line sensitive if blame annotations are visible, otherwise it uses file history - - Removes all debug logging, unless the `gitlens.advanced.output.debug` settings it on + - Improves performance (significantly) when only showing CodeLens at the document level + - Changes `gitlens.diffWithPrevious` command to always be file sensitive diffs + - Changes `gitlens.diffWithWorking` command to always be file sensitive diffs + - Removes all debug logging, unless the `gitlens.advanced.debug` settings it on - Fixes issue where blame annotations would not be cleared properly when switching between open files ### 0.5.5 diff --git a/README.md b/README.md index a0c49d1..cc882cc 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ Provides Git information (most recent commit, # of authors) in CodeLens, on-dema > Clicking on the CodeLens toggles Git blame annotations on/off - Provides on-demand **inline blame annotations** with multiple styles - Provides Git blame information about the selected line in the **status bar** -- Provides a Git **blame history explorer** to visualize the history of a file or block +- Provides a Git **history explorer** to visualize the history of a file or block +- Provides a Git **blame history explorer** to visualize the blame history of a file or block - Provides ability to **compare diffs** with the working tree as well as with previous versions -- Provides many configuration settings to allow the **customization** of almost all Features +- Provides many configuration settings to allow the **customization** of almost all features --- ## Screenshots @@ -38,17 +39,20 @@ Must be using Git and it must be in your path. |`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.languageLocations`|Specifies where CodeLens will be rendered in the active document for the specified languages |`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. `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.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.showHistory` - opens the 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. `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.command`|Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showHistory` - opens the 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.menus.fileDiff.enabled`|Specifies whether file-based diff commands will be added to the context menus +|`gitlens.menus.lineDiff.enabled`|Specifies whether line-based diff commands will be added to the context menus |`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" +|`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.showHistory` - opens the 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 -- Content in the **Blame history explorer** disappears after a bit: [vscode issue](https://github.com/Microsoft/vscode/issues/11360) +- Content in the **history explorers** disappears after a bit: [vscode issue](https://github.com/Microsoft/vscode/issues/11360) - Highlighted lines disappear in **Blame explorer** after changing selection and returning to a previous selection: [vscode issue](https://github.com/Microsoft/vscode/issues/11360) - CodeLens aren't updated properly after a file is saved: [vscode issue](https://github.com/Microsoft/vscode/issues/11546) - Visible whitespace causes issue with blame overlay (currently fixed with a hack, but fails randomly): [vscode issue](https://github.com/Microsoft/vscode/issues/11485) \ No newline at end of file diff --git a/package.json b/package.json index 87bc4c4..917761d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlens", - "version": "0.5.5", + "version": "0.9.0", "author": { "name": "Eric Amodio", "email": "eamodio@gmail.com" @@ -18,7 +18,7 @@ "keywords": [ "git", "blame", - "gitblame", + "history", "codelens", "annotation" ], @@ -27,7 +27,7 @@ "theme": "dark" }, "icon": "images/gitlens-icon.png", - "preview": false, + "preview": true, "homepage": "https://github.com/eamodio/vscode-gitlens/blob/master/README.md", "bugs": { "url": "https://github.com/eamodio/vscode-gitlens/issues" @@ -91,6 +91,64 @@ "type": "array", "description": "Specifies the set of document symbols to render active document CodeLens on. Must be a member of `SymbolKind`" }, + "gitlens.codeLens.languageLocations": { + "type": "array", + "default": [ + { + "language": "json", + "location": "document" + }, + { + "language": "css", + "location": "document" + }, + { + "language": "scss", + "location": "document" + }, + { + "language": "less", + "location": "document" + } + ], + "items": { + "type": "object", + "required": [ + "language", + "location" + ], + "properties": { + "language": { + "type": "string", + "description": "Specifies the language to which this CodeLens override applies" + }, + "location": { + "type": "string", + "default": "document+containers", + "enum": [ + "all", + "document+containers", + "document", + "custom", + "none" + ], + "description": "Specifies where CodeLens will be rendered in the active document for the specified language. `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 `customSymbols`" + }, + "customSymbols": { + "type": "string", + "description": "Specifies the set of document symbols to render active document CodeLens on. Must be a member of `SymbolKind`" + } + } + }, + "uniqueItems": true, + "enum": [ + "all", + "document+containers", + "document", + "custom" + ], + "description": "Specifies where CodeLens will be rendered in the active document for the specified languages" + }, "gitlens.codeLens.recentChange.enabled": { "type": "boolean", "default": true, @@ -98,7 +156,7 @@ }, "gitlens.codeLens.recentChange.command": { "type": "string", - "default": "gitlens.showBlameHistory", + "default": "gitlens.showHistory", "enum": [ "gitlens.toggleBlame", "gitlens.showBlameHistory", @@ -106,7 +164,7 @@ "gitlens.diffWithPrevious", "git.viewFileHistory" ], - "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" + "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.showHistory` - opens the 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", @@ -123,7 +181,7 @@ "gitlens.diffWithPrevious", "git.viewFileHistory" ], - "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" + "description": "Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showHistory` - opens the 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", @@ -141,17 +199,27 @@ "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" + "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.showHistory` - opens the 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.menus.fileDiff.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether file-based diff commands will be added to the context menus" + }, + "gitlens.menus.lineDiff.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether line-based diff commands will be added to the context menus" }, "gitlens.advanced.caching.enabled": { "type": "boolean", "default": true, "description": "Specifies whether git blame output will be cached" }, - "gitlens.advanced.output.debug": { + "gitlens.advanced.debug": { "type": "boolean", "default": false, - "description": "Specifies whether all output will be sent to the console" + "description": "Specifies debug mode" }, "gitlens.advanced.output.level": { "type": "string", @@ -168,12 +236,22 @@ "commands": [ { "command": "gitlens.diffWithPrevious", - "title": "Open Diff with Previous Commit", + "title": "Diff with Previous Commit", + "category": "GitLens" + }, + { + "command": "gitlens.diffLineWithPrevious", + "title": "Diff with Previous Commit (line)", "category": "GitLens" }, { "command": "gitlens.diffWithWorking", - "title": "Open Diff with Working Tree", + "title": "Diff with Working Tree", + "category": "GitLens" + }, + { + "command": "gitlens.diffLineWithWorking", + "title": "Diff with Working Tree (line)", "category": "GitLens" }, { @@ -206,31 +284,42 @@ "explorer/context": [ { "command": "gitlens.diffWithPrevious", - "group": "gitlens@1.1" + "when": "config.gitlens.menus.fileDiff.enabled && config.git.enabled", + "group": "2_gitlens-file" } ], "editor/title": [ { - "when": "editorTextFocus", "command": "gitlens.toggleBlame", - "group": "gitlens" + "when": "editorTextFocus && config.git.enabled", + "group": "2_gitlens-blame" } ], "editor/context": [ { - "when": "editorTextFocus", + "command": "gitlens.diffLineWithWorking", + "when": "editorTextFocus && config.gitlens.menus.lineDiff.enabled && config.git.enabled", + "group": "3_gitlens-line@1.0" + }, + { + "command": "gitlens.diffLineWithPrevious", + "when": "editorTextFocus && config.gitlens.menus.lineDiff.enabled && config.git.enabled", + "group": "3_gitlens-line@1.1" + }, + { "command": "gitlens.diffWithWorking", - "group": "gitlens@1.0" + "when": "editorTextFocus && config.gitlens.menus.fileDiff.enabled && config.git.enabled", + "group": "3_gitlens-file@1.0" }, { - "when": "editorTextFocus", "command": "gitlens.diffWithPrevious", - "group": "gitlens@1.1" + "when": "editorTextFocus && config.gitlens.menus.fileDiff.enabled && config.git.enabled", + "group": "3_gitlens-file@1.1" }, { - "when": "editorTextFocus", "command": "gitlens.toggleBlame", - "group": "gitlens-blame@1.2" + "when": "editorTextFocus && config.git.enabled", + "group": "2_gitlens-blame" } ] }, diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 146730c..abf3a48 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -39,7 +39,7 @@ export default class BlameAnnotationController extends Disposable { return this._annotationProvider !== undefined; } - showBlameAnnotation(editor: TextEditor, sha?: string) { + showBlameAnnotation(editor: TextEditor, sha?: string): Promise { if (!editor || !editor.document || editor.document.isUntitled) { this.clear(); return Promise.resolve(); @@ -53,7 +53,7 @@ export default class BlameAnnotationController extends Disposable { return Promise.resolve(); } - toggleBlameAnnotation(editor: TextEditor, sha?: string) { + toggleBlameAnnotation(editor: TextEditor, sha?: string): Promise { if (!editor || !editor.document || editor.document.isUntitled || this._annotationProvider) { this.clear(); return Promise.resolve(); diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index cb3f8ce..30b7691 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -3,7 +3,7 @@ import { Iterables } from './system'; import { commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace } from 'vscode'; import { BuiltInCommands } from './constants'; import { BlameAnnotationStyle, IBlameConfig } from './configuration'; -import GitProvider, { IGitBlame, IGitCommit } from './gitProvider'; +import GitProvider, { GitCommit, IGitBlame } from './gitProvider'; import * as moment from 'moment'; const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ @@ -218,7 +218,7 @@ export class BlameAnnotationProvider extends Disposable { }); } - private _getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) { + private _getAuthor(commit: GitCommit, max: number = 17, force: boolean = false) { if (!force && !this._config.annotation.author) return ''; let author = commit.isUncommitted ? 'Uncommitted' : commit.author; if (author.length > max) { @@ -227,12 +227,12 @@ export class BlameAnnotationProvider extends Disposable { return author; } - private _getDate(commit: IGitCommit, force?: boolean) { + private _getDate(commit: GitCommit, force?: boolean) { if (!force && !this._config.annotation.date) return ''; return moment(commit.date).format('MM/DD/YYYY'); } - private _getGutter(commit: IGitCommit) { + private _getGutter(commit: GitCommit) { const author = this._getAuthor(commit); const date = this._getDate(commit); if (this._config.annotation.sha) { diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts new file mode 100644 index 0000000..ee4e40f --- /dev/null +++ b/src/commands/diffLineWithPrevious.ts @@ -0,0 +1,52 @@ +'use strict'; +import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { EditorCommand } from './commands'; +import { Commands } from '../constants'; +import GitProvider, { GitCommit } from '../gitProvider'; +import { Logger } from '../logger'; + +export default class DiffLineWithPreviousCommand extends EditorCommand { + constructor(private git: GitProvider) { + super(Commands.DiffLineWithPrevious); + } + + async execute(editor: TextEditor, edit: TextEditorEdit): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + line = line || editor.selection.active.line; + + if (!commit || GitProvider.isUncommitted(commit.sha)) { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + + try { + const blame = await this.git.getBlameForLine(uri.fsPath, line); + if (!blame) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); + + // If the line is uncommitted, find the previous commit + commit = blame.commit; + if (commit.isUncommitted) { + try { + const prevBlame = await this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath); + if (!prevBlame) return undefined; + + const prevCommit = prevBlame.commit; + commit = new GitCommit(commit.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message, commit.lines, commit.originalFileName, prevCommit.sha, prevCommit.fileName); + line = blame.line.originalLine + 1; + } + catch (ex) { + Logger.error('[GitLens.DiffWithPreviousLineCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); + } + } + } + catch (ex) { + Logger.error('[GitLens.DiffWithPreviousLineCommand]', `getBlameForLine(${line})`, ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); + } + } + + return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit, line); + } +} \ No newline at end of file diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts new file mode 100644 index 0000000..9d1ca24 --- /dev/null +++ b/src/commands/diffLineWithWorking.ts @@ -0,0 +1,42 @@ +'use strict'; +import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { EditorCommand } from './commands'; +import { Commands } from '../constants'; +import GitProvider, { GitCommit } from '../gitProvider'; +import { Logger } from '../logger'; + +export default class DiffLineWithWorkingCommand extends EditorCommand { + constructor(private git: GitProvider) { + super(Commands.DiffLineWithWorking); + } + + async execute(editor: TextEditor, edit: TextEditorEdit): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + line = line || editor.selection.active.line; + + if (!commit || GitProvider.isUncommitted(commit.sha)) { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + + try { + const blame = await this.git.getBlameForLine(uri.fsPath, line); + if (!blame) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); + + commit = blame.commit; + // If the line is uncommitted, find the previous commit + if (commit.isUncommitted) { + commit = new GitCommit(commit.repoPath, commit.previousSha, commit.previousFileName, commit.author, commit.date, commit.message); + line = blame.line.line + 1; + } + } + catch (ex) { + Logger.error('[GitLens.DiffLineWithWorkingCommand]', `getBlameForLine(${line})`, ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); + } + } + + return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit, line); + } +} diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 82731cc..c8a1177 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -1,74 +1,60 @@ 'use strict'; import { Iterables } from '../system'; -import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { commands, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import BlameAnnotationController from '../blameAnnotationController'; -import GitProvider from '../gitProvider'; +import GitProvider, { GitCommit } from '../gitProvider'; import { Logger } from '../logger'; +import * as moment from 'moment'; import * as path from 'path'; export default class DiffWithPreviousCommand extends EditorCommand { - constructor(private git: GitProvider, private annotationController: BlameAnnotationController) { + constructor(private git: GitProvider) { super(Commands.DiffWithPrevious); } - async 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 && !GitProvider.isUncommitted(sha)) { - if (!compareWithSha) { - return window.showInformationMessage(`Commit ${sha} has no previous commit`); + async execute(editor: TextEditor, edit: TextEditorEdit): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri, commit: GitCommit, range?: Range): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri, commit: GitCommit, line?: number): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, rangeOrLine?: Range | number): Promise { + let line = editor.selection.active.line; + if (typeof rangeOrLine === 'number') { + line = rangeOrLine || line; + } + + if (!commit || rangeOrLine instanceof Range) { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; } - return Promise.all([this.git.getVersionedFile(shaUri.fsPath, repoPath, sha), this.git.getVersionedFile(compareWithUri.fsPath, repoPath, compareWithSha)]) - .then(values => commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(compareWithUri.fsPath)} (${compareWithSha}) ↔ ${path.basename(shaUri.fsPath)} (${sha})`)) - .then(() => commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' })) - .catch(ex => Logger.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex)); - } - - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } - - if (this.annotationController.annotated) { try { - const blame = await this.git.getBlameForLine(uri.fsPath, line); - if (!blame) return undefined; + const log = await this.git.getLogForFile(uri.fsPath, rangeOrLine); + if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); - // If the line is uncommitted, find the previous commit - const commit = blame.commit; - if (commit.isUncommitted) { - try { - const prevBlame = await this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath); - if (!prevBlame) return undefined; - - const prevCommit = prevBlame.commit; - return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine); - } - catch (ex) { - Logger.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex); - } - } - return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line); - } - catch (ex) { - Logger.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex); - } - } - else { - try { - const log = await this.git.getLogForFile(uri.fsPath); - if (!log) return undefined; - - const commits = log.commits.values(); - const commit = Iterables.next(commits); - const prevCommit = Iterables.next(commits); - return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, prevCommit.sha, prevCommit.uri, line); + commit = commit ? Iterables.find(log.commits.values(), _ => _.sha === commit.sha) : Iterables.first(log.commits.values()); } catch (ex) { Logger.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${uri.fsPath})`, ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } + + if (!commit.previousSha) { + return window.showInformationMessage(`Commit ${commit.sha} (${commit.author}, ${moment(commit.date).fromNow()}) has no previous commit`); + } + + try { + const values = await Promise.all([ + this.git.getVersionedFile(commit.uri.fsPath, commit.repoPath, commit.sha), + this.git.getVersionedFile(commit.previousUri.fsPath, commit.repoPath, commit.previousSha) + ]); + await commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(commit.previousUri.fsPath)} (${commit.previousSha}) ↔ ${path.basename(commit.uri.fsPath)} (${commit.sha})`); + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + } + catch (ex) { + Logger.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); + } } } \ No newline at end of file diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index 61a4b90..d9ac09c 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -1,59 +1,47 @@ 'use strict'; import { Iterables } from '../system'; -import { commands, TextEditor, TextEditorEdit, Uri } from 'vscode'; +import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import BlameAnnotationController from '../blameAnnotationController'; -import GitProvider from '../gitProvider'; +import GitProvider, { GitCommit } from '../gitProvider'; import { Logger } from '../logger'; import * as path from 'path'; export default class DiffWithWorkingCommand extends EditorCommand { - constructor(private git: GitProvider, private annotationController: BlameAnnotationController) { + constructor(private git: GitProvider) { super(Commands.DiffWithWorking); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) { + async execute(editor: TextEditor, edit: TextEditorEdit): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { line = line || editor.selection.active.line; - if (sha && !GitProvider.isUncommitted(sha)) { - return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha) - .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' })) - .catch(ex => Logger.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex)); - } - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } - - if (this.annotationController.annotated) { - try { - const blame = await this.git.getBlameForLine(uri.fsPath, line); - if (!blame) return undefined; - - const commit = blame.commit; - // If the line is uncommitted, find the previous commit - if (commit.isUncommitted) { - return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line + 1); - } - return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line); + if (!commit || GitProvider.isUncommitted(commit.sha)) { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; } - catch (ex) { - Logger.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex); - } - } - else { + try { const log = await this.git.getLogForFile(uri.fsPath); - if (!log) return undefined; + if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); - const commit = Iterables.first(log.commits.values()); - return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line); + commit = Iterables.first(log.commits.values()); } catch (ex) { - Logger.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${uri.fsPath})`, ex); + Logger.error('[GitLens.DiffWithWorkingCommand]', `getLogForFile(${uri.fsPath})`, ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } + + try { + const compare = await this.git.getVersionedFile(commit.uri.fsPath, commit.repoPath, commit.sha); + await commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(commit.uri.fsPath)} (${commit.sha}) ↔ ${path.basename(uri.fsPath)}`); + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + } + catch (ex) { + Logger.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex); + return window.showErrorMessage(`Unable to open diff. See output channel for more details`); + } } } diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts index a6b94ec..edd2c18 100644 --- a/src/commands/showBlame.ts +++ b/src/commands/showBlame.ts @@ -1,5 +1,5 @@ 'use strict'; -import { TextEditor, TextEditorEdit, Uri } from 'vscode'; +import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import BlameAnnotationController from '../blameAnnotationController'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; @@ -23,10 +23,11 @@ export default class ShowBlameCommand extends EditorCommand { try { const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line); - this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha); + return this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha); } catch (ex) { Logger.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex); + return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); } } } \ No newline at end of file diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index 9dd259e..d4126ba 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -1,5 +1,5 @@ 'use strict'; -import { commands, Position, Range, TextEditor, TextEditorEdit, Uri } from 'vscode'; +import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; import GitProvider from '../gitProvider'; @@ -22,10 +22,13 @@ export default class ShowBlameHistoryCommand extends EditorCommand { try { const locations = await this.git.getBlameLocations(uri.fsPath, range); + if (!locations) return window.showWarningMessage(`Unable to show blame history. File is probably not under source control`); + return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); } catch (ex) { Logger.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex); + return window.showErrorMessage(`Unable to show blame history. See output channel for more details`); } } } \ No newline at end of file diff --git a/src/commands/showHistory.ts b/src/commands/showHistory.ts index 37397ce..66bee61 100644 --- a/src/commands/showHistory.ts +++ b/src/commands/showHistory.ts @@ -1,5 +1,5 @@ 'use strict'; -import { commands, Position, Range, TextEditor, TextEditorEdit, Uri } from 'vscode'; +import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; import GitProvider from '../gitProvider'; @@ -10,7 +10,7 @@ export default class ShowHistoryCommand extends EditorCommand { super(Commands.ShowHistory); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, position?: Position) { + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, position?: Position, sha?: string, line?: number) { if (!(uri instanceof Uri)) { if (!editor.document) return undefined; uri = editor.document.uri; @@ -20,11 +20,14 @@ export default class ShowHistoryCommand extends EditorCommand { } try { - const locations = await this.git.getLogLocations(uri.fsPath); + const locations = await this.git.getLogLocations(uri.fsPath, sha, line); + if (!locations) return window.showWarningMessage(`Unable to show history. File is probably not under source control`); + return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); } catch (ex) { Logger.error('[GitLens.ShowHistoryCommand]', 'getLogLocations', ex); + return window.showErrorMessage(`Unable to show history. See output channel for more details`); } } } \ No newline at end of file diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts index 7c1a3c8..1c736d3 100644 --- a/src/commands/toggleBlame.ts +++ b/src/commands/toggleBlame.ts @@ -1,5 +1,5 @@ 'use strict'; -import { TextEditor, TextEditorEdit, Uri } from 'vscode'; +import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import BlameAnnotationController from '../blameAnnotationController'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; @@ -23,10 +23,11 @@ export default class ToggleBlameCommand extends EditorCommand { try { const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line); - this.annotationController.toggleBlameAnnotation(editor, blame && blame.commit.sha); + return this.annotationController.toggleBlameAnnotation(editor, blame && blame.commit.sha); } catch (ex) { Logger.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex); + return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); } } } \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index 4bde0b7..6770e77 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -19,17 +19,19 @@ export interface IBlameConfig { export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory'; export const CodeLensCommand = { BlameAnnotate: Commands.ToggleBlame as CodeLensCommand, - BlameExplorer: Commands.ShowBlameHistory as CodeLensCommand, + ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand, + ShowHistory: Commands.ShowHistory as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand, GitViewHistory: 'git.viewFileHistory' as CodeLensCommand }; -export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom'; +export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom' | 'none'; export const CodeLensLocation = { All: 'all' as CodeLensLocation, DocumentAndContainers: 'document+containers' as CodeLensLocation, Document: 'document' as CodeLensLocation, - Custom: 'custom' as CodeLensLocation + Custom: 'custom' as CodeLensLocation, + None: 'none' as CodeLensLocation }; export type CodeLensVisibility = 'auto' | 'ondemand' | 'off'; @@ -44,10 +46,17 @@ export interface ICodeLensConfig { command: CodeLensCommand; } +export interface ICodeLensLanguageLocation { + language: string; + location: CodeLensLocation; + customSymbols?: string[]; +} + export interface ICodeLensesConfig { visibility: CodeLensVisibility; location: CodeLensLocation; locationCustomSymbols: string[]; + languageLocations: ICodeLensLanguageLocation[]; recentChange: ICodeLensConfig; authors: ICodeLensConfig; } @@ -77,8 +86,8 @@ export interface IAdvancedConfig { caching: { enabled: boolean; }; + debug: boolean; output: { - debug: boolean; level: OutputLevel; }; } diff --git a/src/constants.ts b/src/constants.ts index 3bd482b..15e2f69 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,10 +14,12 @@ export const BuiltInCommands = { ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands }; -export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; +export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; export const Commands = { DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, + DiffLineWithPrevious: 'gitlens.diffLineWithPrevious' as Commands, DiffWithWorking: 'gitlens.diffWithWorking' as Commands, + DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, ShowBlame: 'gitlens.showBlame' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, ShowHistory: 'gitlens.showHistory' as Commands, diff --git a/src/extension.ts b/src/extension.ts index 57669ee..b30753b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,9 @@ import GitBlameContentProvider from './gitBlameContentProvider'; import GitProvider, { Git } from './gitProvider'; import { WorkspaceState } from './constants'; import DiffWithPreviousCommand from './commands/diffWithPrevious'; +import DiffLineWithPreviousCommand from './commands/diffLineWithPrevious'; import DiffWithWorkingCommand from './commands/diffWithWorking'; +import DiffLineWithWorkingCommand from './commands/diffLineWithWorking'; import ShowBlameCommand from './commands/showBlame'; import ShowBlameHistoryCommand from './commands/showBlameHistory'; import ShowHistoryCommand from './commands/showHistory'; @@ -46,8 +48,10 @@ export function activate(context: ExtensionContext) { const statusBarController = new BlameStatusBarController(context, git); context.subscriptions.push(statusBarController); - context.subscriptions.push(new DiffWithWorkingCommand(git, annotationController)); - context.subscriptions.push(new DiffWithPreviousCommand(git, annotationController)); + context.subscriptions.push(new DiffWithWorkingCommand(git)); + context.subscriptions.push(new DiffLineWithWorkingCommand(git)); + context.subscriptions.push(new DiffWithPreviousCommand(git)); + context.subscriptions.push(new DiffLineWithPreviousCommand(git)); context.subscriptions.push(new ShowBlameCommand(git, annotationController)); context.subscriptions.push(new ToggleBlameCommand(git, annotationController)); context.subscriptions.push(new ShowBlameHistoryCommand(git)); diff --git a/src/git/enrichers/blameParserEnricher.ts b/src/git/enrichers/blameParserEnricher.ts index 856db5b..2e69b6b 100644 --- a/src/git/enrichers/blameParserEnricher.ts +++ b/src/git/enrichers/blameParserEnricher.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommit, IGitCommitLine, IGitEnricher } from './../git'; +import { GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommitLine, IGitEnricher } from './../git'; import * as moment from 'moment'; import * as path from 'path'; @@ -123,7 +123,7 @@ export class GitBlameParserEnricher implements IGitEnricher { if (!entries) return null; const authors: Map = new Map(); - const commits: Map = new Map(); + const commits: Map = new Map(); const lines: Array = []; let repoPath: string; diff --git a/src/git/enrichers/logParserEnricher.ts b/src/git/enrichers/logParserEnricher.ts index 9b504e3..2a426f6 100644 --- a/src/git/enrichers/logParserEnricher.ts +++ b/src/git/enrichers/logParserEnricher.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitCommit, IGitAuthor, IGitCommit, IGitEnricher, IGitLog } from './../git'; +import { GitCommit, IGitAuthor, IGitEnricher, IGitLog } from './../git'; import * as moment from 'moment'; import * as path from 'path'; @@ -35,6 +35,7 @@ export class GitLogParserEnricher implements IGitEnricher { } if (!entry) { + if (!/^[a-f0-9]{40}$/.test(lineParts[0])) continue; entry = { sha: lineParts[0].substring(0, 8) }; @@ -66,7 +67,13 @@ export class GitLogParserEnricher implements IGitEnricher { case 'filename': position += 2; lineParts = lines[position].split(' '); - entry.fileName = lineParts.join(' '); + if (lineParts.length === 1) { + entry.fileName = lineParts[0]; + } + else { + entry.fileName = lineParts[3].substring(2); + position += 4; + } entries.push(entry); entry = null; @@ -85,10 +92,11 @@ export class GitLogParserEnricher implements IGitEnricher { if (!entries) return null; const authors: Map = new Map(); - const commits: Map = new Map(); + const commits: Map = new Map(); let repoPath: string; let relativeFileName: string; + let recentCommit: GitCommit; for (let i = 0, len = entries.length; i < len; i++) { const entry = entries[i]; @@ -118,6 +126,12 @@ export class GitLogParserEnricher implements IGitEnricher { commits.set(entry.sha, commit); } + + if (recentCommit) { + recentCommit.previousSha = commit.sha; + recentCommit.previousFileName = commit.originalFileName || commit.fileName; + } + recentCommit = commit; } commits.forEach(c => authors.get(c.author).lineCount += c.lines.length); diff --git a/src/git/git.ts b/src/git/git.ts index 2c7bf20..ee10d68 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -77,7 +77,13 @@ export default class Git { static log(fileName: string, repoPath?: string) { const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); - return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename -`, file); + return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, file); + } + + static logRange(fileName: string, start: number, end: number, repoPath?: string) { + const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); + + return gitCommand(root, 'log', `--name-only`, `--no-merges`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, `-L ${start},${end}:${file}`); } static getVersionedFile(fileName: string, repoPath: string, sha: string) { diff --git a/src/git/gitEnrichment.ts b/src/git/gitEnrichment.ts index 8eef91d..386fd1e 100644 --- a/src/git/gitEnrichment.ts +++ b/src/git/gitEnrichment.ts @@ -1,5 +1,5 @@ 'use strict'; -import {Uri} from 'vscode'; +import { Uri } from 'vscode'; import Git from './git'; import * as path from 'path'; @@ -10,13 +10,13 @@ export interface IGitEnricher { export interface IGitBlame { repoPath: string; authors: Map; - commits: Map; + commits: Map; lines: IGitCommitLine[]; } export interface IGitBlameLine { author: IGitAuthor; - commit: IGitCommit; + commit: GitCommit; line: IGitCommitLine; } @@ -26,7 +26,7 @@ export interface IGitBlameLines extends IGitBlame { export interface IGitBlameCommitLines { author: IGitAuthor; - commit: IGitCommit; + commit: GitCommit; lines: IGitCommitLine[]; } @@ -35,7 +35,7 @@ export interface IGitAuthor { lineCount: number; } -export interface IGitCommit { +interface IGitCommit { repoPath: string; sha: string; fileName: string; @@ -57,10 +57,20 @@ export class GitCommit implements IGitCommit { originalFileName?: string; previousSha?: string; previousFileName?: string; - private _isUncommitted: boolean|undefined; + private _isUncommitted: boolean | undefined; - constructor(public repoPath: string, public sha: string, public fileName: string, public author: string, public date: Date, public message: string, - lines?: IGitCommitLine[], originalFileName?: string, previousSha?: string, previousFileName?: string) { + constructor( + public repoPath: string, + public sha: string, + public fileName: string, + public author: string, + public date: Date, + public message: string, + lines?: IGitCommitLine[], + originalFileName?: string, + previousSha?: string, + previousFileName?: string + ) { this.lines = lines || []; this.originalFileName = originalFileName; this.previousSha = previousSha; @@ -94,5 +104,5 @@ export interface IGitCommitLine { export interface IGitLog { repoPath: string; authors: Map; - commits: Map; + commits: Map; } \ No newline at end of file diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts index cf770c9..77b10b9 100644 --- a/src/gitBlameCodeLensProvider.ts +++ b/src/gitBlameCodeLensProvider.ts @@ -1,17 +1,17 @@ 'use strict'; import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode'; import { Commands, DocumentSchemes } from './constants'; -import GitProvider, { IGitCommit } from './gitProvider'; +import GitProvider, { GitCommit } from './gitProvider'; import * as path from 'path'; export class GitDiffWithWorkingTreeCodeLens extends CodeLens { - constructor(git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) { + constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { super(range); } } export class GitDiffWithPreviousCodeLens extends CodeLens { - constructor(git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) { + constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { super(range); } } @@ -84,11 +84,7 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider { command: Commands.DiffWithPrevious, arguments: [ Uri.file(lens.fileName), - lens.commit.repoPath, - lens.commit.sha, - lens.commit.uri, - lens.commit.previousSha, - lens.commit.previousUri, + lens.commit, lens.range.start.line ] }; diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 641c2dc..4132abb 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -2,12 +2,12 @@ import { Iterables, Strings } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; import { BuiltInCommands, Commands, DocumentSchemes, WorkspaceState } from './constants'; -import { CodeLensCommand, CodeLensLocation, ICodeLensesConfig } from './configuration'; -import GitProvider, {IGitBlame, IGitBlameLines} from './gitProvider'; +import { CodeLensCommand, CodeLensLocation, IConfig, ICodeLensLanguageLocation } from './configuration'; +import GitProvider, { GitCommit, IGitBlame, IGitBlameLines, IGitLog } from './gitProvider'; import * as moment from 'moment'; export class GitRecentChangeCodeLens extends CodeLens { - constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, range: Range) { + constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { super(range); } @@ -17,7 +17,7 @@ export class GitRecentChangeCodeLens extends CodeLens { } export class GitAuthorsCodeLens extends CodeLens { - constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, range: Range) { + constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { super(range); } @@ -29,45 +29,67 @@ export class GitAuthorsCodeLens extends CodeLens { export default class GitCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.File }; - private _config: ICodeLensesConfig; + private _config: IConfig; private _hasGitHistoryExtension: boolean; constructor(context: ExtensionContext, private git: GitProvider) { - this._config = workspace.getConfiguration('gitlens').get('codeLens'); + this._config = workspace.getConfiguration('').get('gitlens'); this._hasGitHistoryExtension = context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false); - } - - provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { - const fileName = document.fileName; - const promise = Promise.all([this.git.getBlameForFile(fileName) as Promise, (commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise)]); - - return promise.then(values => { - const blame = values[0] as IGitBlame; - if (!blame || !blame.lines.length) return []; - - const symbols = values[1] as SymbolInformation[]; - const lenses: CodeLens[] = []; - symbols.forEach(sym => this._provideCodeLens(fileName, document, sym, lenses)); - - if (this._config.location !== CodeLensLocation.Custom || (this._config.locationCustomSymbols || []).find(_ => _.toLowerCase() === 'file')) { - // 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)); - 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; - }); } - private _isValidSymbol(kind: SymbolKind) { - switch (this._config.location) { + async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language.toLowerCase() === document.languageId); + if (languageLocations == null) { + languageLocations = { + language: null, + location: this._config.codeLens.location, + customSymbols: this._config.codeLens.locationCustomSymbols + }; + } + + const lenses: CodeLens[] = []; + + if (languageLocations.location === CodeLensLocation.None) return lenses; + + const fileName = document.fileName; + + const blamePromise = this.git.getBlameForFile(fileName); + let blame: IGitBlame; + if (languageLocations.location === CodeLensLocation.Document) { + blame = await blamePromise; + if (!blame || !blame.lines.length) return lenses; + } + else { + const values = await Promise.all([ + >blamePromise, + >commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) + ]); + + blame = values[0] as IGitBlame; + if (!blame || !blame.lines.length) return lenses; + + const symbols = values[1] as SymbolInformation[]; + symbols.forEach(sym => this._provideCodeLens(fileName, document, sym, languageLocations, lenses)); + } + + if (languageLocations.location !== CodeLensLocation.Custom || (languageLocations.customSymbols || []).find(_ => _.toLowerCase() === 'file')) { + // 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)); + if (this._config.codeLens.recentChange.enabled) { + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character))); + } + if (this._config.codeLens.authors.enabled) { + lenses.push(new GitAuthorsCodeLens(this.git, fileName, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); + } + } + } + + return lenses; + } + + private _isValidSymbol(kind: SymbolKind, languageLocation: ICodeLensLanguageLocation) { + switch (languageLocation.location) { case CodeLensLocation.All: case CodeLensLocation.DocumentAndContainers: switch (kind) { @@ -83,20 +105,20 @@ export default class GitCodeLensProvider implements CodeLensProvider { case SymbolKind.Function: case SymbolKind.Property: case SymbolKind.Enum: - return this._config.location === CodeLensLocation.All; + return languageLocation.location === CodeLensLocation.All; default: return false; } case CodeLensLocation.Document: return false; case CodeLensLocation.Custom: - return !!(this._config.locationCustomSymbols || []).find(_ => _.toLowerCase() === SymbolKind[kind].toLowerCase()); + return !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[kind].toLowerCase()); } return false; } - private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void { - if (!this._isValidSymbol(symbol.kind)) return; + private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, lenses: CodeLens[]): void { + if (!this._isValidSymbol(symbol.kind, languageLocation)) return; const line = document.lineAt(symbol.location.range.start); // Make sure there is only 1 lense per line @@ -115,15 +137,15 @@ export default class GitCodeLensProvider implements CodeLensProvider { startChar += Math.floor(symbol.name.length / 2); } - 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)))); + if (this._config.codeLens.recentChange.enabled) { + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar)))); startChar++; } - if (this._config.authors.enabled) { + if (this._config.codeLens.authors.enabled) { // HACK for Omnisharp, since it doesn't return full ranges let multiline = (symbol.location.range.end.line - symbol.location.range.start.line) > 1; - if (!multiline && fileName.endsWith('.cs')) { + if (!multiline && document.languageId === 'csharp') { switch (symbol.kind) { case SymbolKind.File: case SymbolKind.Package: @@ -141,7 +163,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { } if (multiline) { - lenses.push(new GitAuthorsCodeLens(this.git, fileName, symbol.kind, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + lenses.push(new GitAuthorsCodeLens(this.git, fileName, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar)))); } } } @@ -152,44 +174,51 @@ export default class GitCodeLensProvider implements CodeLensProvider { return Promise.reject(null); } - _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable { - return lens.getBlame().then(blame => { - const recentCommit = Iterables.first(blame.commits.values()); - 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.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); - case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); - default: return lens; - } - }); + async _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Promise { + const blame = await lens.getBlame(); + + const recentCommit = Iterables.first(blame.commits.values()); + let title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; + if (this._config.advanced.debug) { + title += ` [${recentCommit.sha}, Symbol(${SymbolKind[lens.symbolKind]}), Lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})]`; + } + + switch (this._config.codeLens.recentChange.command) { + case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand(title, lens, blame); + case CodeLensCommand.ShowBlameHistory: return this._applyShowBlameHistoryCommand(title, lens, blame); + case CodeLensCommand.ShowHistory: return this._applyShowHistoryCommand(title, lens, blame, recentCommit); + case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame, recentCommit); + case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); + default: return lens; + } } - _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): Thenable { - return lens.getBlame().then(blame => { - const count = blame.authors.size; - const title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).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.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); - case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); - default: return lens; - } - }); + async _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): Promise { + const blame = await lens.getBlame(); + + const count = blame.authors.size; + const title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`; + + switch (this._config.codeLens.authors.command) { + case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand(title, lens, blame); + case CodeLensCommand.ShowBlameHistory: return this._applyShowBlameHistoryCommand(title, lens, blame); + case CodeLensCommand.ShowHistory: return this._applyShowHistoryCommand(title, lens, blame); + case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); + case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); + default: return lens; + } } - _applyBlameAnnotateCommand(title: string, lens: T, blame: IGitBlameLines, sha?: string) { + _applyBlameAnnotateCommand(title: string, lens: T, blame: IGitBlameLines) { lens.command = { title: title, command: Commands.ToggleBlame, - arguments: [Uri.file(lens.fileName), sha] + arguments: [Uri.file(lens.fileName)] }; return lens; } - _applyBlameExplorerCommand(title: string, lens: T, blame: IGitBlameLines) { + _applyShowBlameHistoryCommand(title: string, lens: T, blame: IGitBlameLines) { lens.command = { title: title, command: Commands.ShowBlameHistory, @@ -198,28 +227,42 @@ 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); + _applyShowHistoryCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit) { + let line = lens.range.start.line; + const blameLine = commit.lines.find(_ => _.line === line); + if (blameLine) { + line = blameLine.originalLine; + } + + const position = lens.isFullRange ? new Position(1, 0) : lens.range.start; + lens.command = { + title: title, + command: Commands.ShowHistory, + arguments: [Uri.file(lens.fileName), position, commit.sha, line] + }; + return lens; + } + + async _applyDiffWithPreviousCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): Promise { + if (!commit) { + const blameLine = blame.allLines[lens.range.start.line]; + commit = blame.commits.get(blameLine.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 + commit, + lens.isFullRange ? undefined : lens.blameRange ] }; return lens; } _applyGitHistoryCommand(title: string, lens: T, blame: IGitBlameLines) { - if (!this._hasGitHistoryExtension) return this._applyBlameExplorerCommand(title, lens, blame); + if (!this._hasGitHistoryExtension) return this._applyShowHistoryCommand(title, lens, blame); lens.command = { title: title, diff --git a/src/gitProvider.ts b/src/gitProvider.ts index dd2315c..0f3cd10 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -4,7 +4,7 @@ import { Disposable, DocumentFilter, ExtensionContext, languages, Location, Posi import { DocumentSchemes, WorkspaceState } from './constants'; import { CodeLensVisibility, IConfig } from './configuration'; import GitCodeLensProvider from './gitCodeLensProvider'; -import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameCommitLines, IGitBlameLine, IGitBlameLines, IGitCommit, IGitLog } from './git/git'; +import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameCommitLines, IGitBlameLine, IGitBlameLines, IGitLog } from './git/git'; import { Logger } from './logger'; import * as fs from 'fs'; import * as ignore from 'ignore'; @@ -96,7 +96,11 @@ export default class GitProvider extends Disposable { private _onConfigure() { const config = workspace.getConfiguration().get('gitlens'); - if (!Objects.areEquivalent(config.codeLens, this._config && this._config.codeLens)) { + const codeLensChanged = !Objects.areEquivalent(config.codeLens, this._config && this._config.codeLens); + const advancedChanged = !Objects.areEquivalent(config.advanced, this._config && this._config.advanced); + + if (codeLensChanged || advancedChanged) { + Logger.log('CodeLens config changed; resetting CodeLens provider'); this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); if (config.codeLens.visibility === CodeLensVisibility.Auto && (config.codeLens.recentChange.enabled || config.codeLens.authors.enabled)) { this._codeLensProviderSelector = GitCodeLensProvider.selector; @@ -106,7 +110,7 @@ export default class GitProvider extends Disposable { } } - if (!Objects.areEquivalent(config.advanced, this._config && this._config.advanced)) { + if (advancedChanged) { if (config.advanced.caching.enabled) { // TODO: Cache needs to be cleared on file changes -- createFileSystemWatcher or timeout? this._cache = new Map(); @@ -163,28 +167,29 @@ export default class GitProvider extends Disposable { return Git.repoPath(cwd); } - async getBlameForFile(fileName: string): Promise { + getBlameForFile(fileName: string): Promise { Logger.log(`getBlameForFile('${fileName}')`); fileName = Git.normalizePath(fileName); - const cacheKey = this._getCacheEntryKey(fileName); - let entry: CacheEntry | undefined = undefined; + let cacheKey: string | undefined; + let entry: CacheEntry | undefined; if (this.UseCaching) { + cacheKey = this._getCacheEntryKey(fileName); entry = this._cache.get(cacheKey); + if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; if (entry === undefined) { entry = new CacheEntry(); } } - const ignore = await this._gitignore; - let blame: Promise; - if (ignore && !ignore.filter([fileName]).length) { - Logger.log(`Skipping blame; '${fileName}' is gitignored`); - blame = GitProvider.EmptyPromise; - } - else { - blame = Git.blame(GitProvider.BlameFormat, fileName) + const promise = this._gitignore.then(ignore => { + if (ignore && !ignore.filter([fileName]).length) { + Logger.log(`Skipping blame; '${fileName}' is gitignored`); + return >GitProvider.EmptyPromise; + } + + return Git.blame(GitProvider.BlameFormat, fileName) .then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName)) .catch(ex => { // Trap and cache expected blame errors @@ -199,24 +204,24 @@ export default class GitProvider extends Disposable { }; this._cache.set(cacheKey, entry); - return GitProvider.EmptyPromise; + return >GitProvider.EmptyPromise; } return null; }); - } + }); if (this.UseCaching) { - Logger.log(`Add ${(blame === GitProvider.EmptyPromise ? 'empty promise to ' : '')}blame cache for '${cacheKey}'`); + Logger.log(`Add blame cache for '${cacheKey}'`); entry.blame = { //date: new Date(), - item: blame + item: promise }; this._cache.set(cacheKey, entry); } - return blame; + return promise; } async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { @@ -274,11 +279,11 @@ export default class GitProvider extends Disposable { lines.forEach(l => shas.add(l.sha)); const authors: Map = new Map(); - const commits: Map = new Map(); + const commits: Map = new Map(); blame.commits.forEach(c => { if (!shas.has(c.sha)) return; - const commit: IGitCommit = new GitCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, + const commit: GitCommit = new GitCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName); commits.set(c.sha, commit); @@ -346,32 +351,35 @@ export default class GitProvider extends Disposable { return locations; } - async getLogForFile(fileName: string) { - Logger.log(`getLogForFile('${fileName}')`); + getLogForFile(fileName: string, range?: Range): Promise { + Logger.log(`getLogForFile('${fileName}', ${range})`); fileName = Git.normalizePath(fileName); - const cacheKey = this._getCacheEntryKey(fileName); - let entry: CacheEntry = undefined; - if (this.UseCaching) { + const useCaching = this.UseCaching && !range; + + let cacheKey: string; + let entry: CacheEntry; + if (useCaching) { + cacheKey = this._getCacheEntryKey(fileName); entry = this._cache.get(cacheKey); + if (entry !== undefined && entry.log !== undefined) return entry.log.item; if (entry === undefined) { entry = new CacheEntry(); } } - const ignore = await this._gitignore; - let log: Promise; - if (ignore && !ignore.filter([fileName]).length) { - Logger.log(`Skipping log; '${fileName}' is gitignored`); - log = GitProvider.EmptyPromise; - } - else { - log = Git.log(fileName) + const promise = this._gitignore.then(ignore => { + if (ignore && !ignore.filter([fileName]).length) { + Logger.log(`Skipping log; '${fileName}' is gitignored`); + return >GitProvider.EmptyPromise; + } + + return (range ? Git.logRange(fileName, range.start.line + 1, range.end.line + 1) : Git.log(fileName)) .then(data => new GitLogParserEnricher().enrich(data, fileName)) .catch(ex => { // Trap and cache expected blame errors - if (this.UseCaching) { + if (useCaching) { const msg = ex && ex.toString(); Logger.log(`Replace log cache with empty promise for '${cacheKey}'`); @@ -382,28 +390,28 @@ export default class GitProvider extends Disposable { }; this._cache.set(cacheKey, entry); - return GitProvider.EmptyPromise; + return >GitProvider.EmptyPromise; } return null; }); - } + }); - if (this.UseCaching) { - Logger.log(`Add ${(log === GitProvider.EmptyPromise ? 'empty promise to ' : '')}log cache for '${cacheKey}'`); + if (useCaching) { + Logger.log(`Add log cache for '${cacheKey}'`); entry.log = { //date: new Date(), - item: log + item: promise }; this._cache.set(cacheKey, entry); } - return log; + return promise; } - async getLogLocations(fileName: string): Promise { - Logger.log(`getLogLocations('${fileName}')`); + async getLogLocations(fileName: string, sha?: string, line?: number): Promise { + Logger.log(`getLogLocations('${fileName}', ${sha}, ${line})`); const log = await this.getLogForFile(fileName); if (!log) return null; @@ -413,12 +421,14 @@ export default class GitProvider extends Disposable { const locations: Array = []; Iterables.forEach(log.commits.values(), (c, i) => { if (c.isUncommitted) return; - - const decoration = `/*\n ${c.sha} - ${c.message}\n ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}\n */`; - locations.push(new Location(c.originalFileName + const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; + const uri = c.originalFileName ? GitProvider.toGitUri(c, i + 1, commitCount, c.originalFileName, decoration) - : GitProvider.toGitUri(c, i + 1, commitCount, undefined, decoration), - new Position(2, 0))); + : GitProvider.toGitUri(c, i + 1, commitCount, undefined, decoration); + locations.push(new Location(uri, new Position(0, 0))); + if (c.sha === sha) { + locations.push(new Location(uri, new Position(line + 1, 0))); + } }); return locations; @@ -486,26 +496,30 @@ export default class GitProvider extends Disposable { return JSON.parse(uri.query) as T; } - static toBlameUri(commit: IGitCommit, index: number, commitCount: number, range: Range, originalFileName?: string) { + static toBlameUri(commit: GitCommit, index: number, commitCount: number, range: Range, originalFileName?: string) { return GitProvider._toGitUri(commit, DocumentSchemes.GitBlame, commitCount, GitProvider._toGitBlameUriData(commit, index, range, originalFileName)); } - static toGitUri(commit: IGitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string) { + static toGitUri(commit: GitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string) { return GitProvider._toGitUri(commit, DocumentSchemes.Git, commitCount, GitProvider._toGitUriData(commit, index, originalFileName, decoration)); } - private static _toGitUri(commit: IGitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData | IGitBlameUriData) { + private static _toGitUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData | IGitBlameUriData) { const pad = (n: number) => ('0000000' + n).slice(-('' + commitCount).length); const ext = path.extname(data.fileName); // const uriPath = `${dirname(data.fileName)}/${commit.sha}: ${basename(data.fileName, ext)}${ext}`; - const uriPath = `${path.dirname(data.fileName)}/${commit.sha}${ext}`; + const uriPath = `${path.relative(commit.repoPath, data.fileName.slice(0, -ext.length))}/${commit.sha}${ext}`; + let message = commit.message; + if (message.length > 50) { + message = message.substring(0, 49) + '\u2026'; + } // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location //return Uri.parse(`${scheme}:${pad(data.index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MMa')} - ${uriPath}?${JSON.stringify(data)}`); - return Uri.parse(`${scheme}:${pad(data.index)}. ${moment(commit.date).format('MMM D, YYYY hh:MMa')} - ${uriPath}?${JSON.stringify(data)}`); + return Uri.parse(`${scheme}:${pad(data.index)} \u2022 ${message} \u2022 ${moment(commit.date).format('MMM D, YYYY hh:MMa')} \u2022 ${uriPath}?${JSON.stringify(data)}`); } - private static _toGitUriData(commit: IGitCommit, index: number, originalFileName?: string, decoration?: string): T { + private static _toGitUriData(commit: GitCommit, index: number, originalFileName?: string, decoration?: string): T { const fileName = Git.normalizePath(path.join(commit.repoPath, commit.fileName)); const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha, index: index } as T; if (originalFileName) { @@ -517,7 +531,7 @@ export default class GitProvider extends Disposable { return data; } - private static _toGitBlameUriData(commit: IGitCommit, index: number, range: Range, originalFileName?: string) { + private static _toGitBlameUriData(commit: GitCommit, index: number, range: Range, originalFileName?: string) { const data = this._toGitUriData(commit, index, originalFileName); data.range = range; return data; diff --git a/src/logger.ts b/src/logger.ts index 6bb39fc..e48cee4 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -26,7 +26,7 @@ function onConfigurationChange() { export class Logger { static log(message?: any, ...params: any[]): void { - if (config.output.debug) { + if (config.debug) { console.log('[GitLens]', message, ...params); } @@ -36,7 +36,7 @@ export class Logger { } static error(message?: any, ...params: any[]): void { - if (config.output.debug) { + if (config.debug) { console.error('[GitLens]', message, ...params); } @@ -46,7 +46,7 @@ export class Logger { } static warn(message?: any, ...params: any[]): void { - if (config.output.debug) { + if (config.debug) { console.warn('[GitLens]', message, ...params); } diff --git a/src/system/iterable.ts b/src/system/iterable.ts index 537b6c7..4f11259 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -43,6 +43,12 @@ export namespace Iterables { return typeof source[Symbol.iterator] === 'function'; } + export function last(source: Iterable): T { + let item: T; + for (item of source) { /* noop */ } + return item; + } + export function* map(source: Iterable | IterableIterator, mapper: (item: T) => TMapped): Iterable { for (const item of source) { yield mapper(item);