diff --git a/CHANGELOG.md b/CHANGELOG.md index a936117..f92dd73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,22 @@ ### 0.9.0 - 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 support for blame annotations and git commands on file revisions + - Adds ability to show multiple blame annotation at the same time (one per vscode editor) + - Adds new `gitlens.showFileHistory` command to open the history explorer + - Adds new `gitlens.showFileHistory` option to the `gitlens.codeLens.recentChange.command`, `gitlens.codeLens.authors.command`, and `gitlens.statusBar.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 + - Complete rewrite of the blame annotation provider to reduce overhead and provide better performance - Improves performance (significantly) when only showing CodeLens at the document level + - Improves performance of status bar blame support - 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 many (most?) issues with whitespace toggling (required because of https://github.com/Microsoft/vscode/issues/11485) - 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 cc882cc..fa5afe9 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,13 @@ Must be using Git and it must be in your path. |`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.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.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.showFileHistory` - opens the file 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.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.command`|Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file 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.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.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.showFileHistory` - opens the file 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 diff --git a/package.json b/package.json index 9685718..091c1ee 100644 --- a/package.json +++ b/package.json @@ -156,15 +156,15 @@ }, "gitlens.codeLens.recentChange.command": { "type": "string", - "default": "gitlens.showHistory", + "default": "gitlens.showFileHistory", "enum": [ "gitlens.toggleBlame", "gitlens.showBlameHistory", - "gitlens.showHistory", + "gitlens.showFileHistory", "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.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" + "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.showFileHistory` - opens the file 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", @@ -177,11 +177,11 @@ "enum": [ "gitlens.toggleBlame", "gitlens.showBlameHistory", - "gitlens.showHistory", + "gitlens.showFileHistory", "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.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" + "description": "Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file 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", @@ -194,12 +194,12 @@ "enum": [ "gitlens.toggleBlame", "gitlens.showBlameHistory", - "gitlens.showHistory", + "gitlens.showFileHistory", "gitlens.diffWithPrevious", "gitlens.toggleCodeLens", "git.viewFileHistory" ], - "description": "Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.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" + "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.showFileHistory` - opens the file 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", @@ -208,7 +208,7 @@ }, "gitlens.menus.lineDiff.enabled": { "type": "boolean", - "default": true, + "default": false, "description": "Specifies whether line-based diff commands will be added to the context menus" }, "gitlens.advanced.caching.enabled": { @@ -216,6 +216,11 @@ "default": true, "description": "Specifies whether git blame output will be cached" }, + "gitlens.advanced.caching.statusBar.maxLines": { + "type": "number", + "default": 0, + "description": "Specifies whether status bar git blame output will be cached for larger documents" + }, "gitlens.advanced.debug": { "type": "boolean", "default": false, @@ -280,8 +285,8 @@ "category": "GitLens" }, { - "command": "gitlens.showHistory", - "title": "Open Git History", + "command": "gitlens.showFileHistory", + "title": "Open Git File History", "category": "GitLens" } ], @@ -289,6 +294,7 @@ "explorer/context": [ { "command": "gitlens.diffWithPrevious", + "alt": "gitlens.diffWithWorking", "when": "config.gitlens.menus.fileDiff.enabled && config.git.enabled", "group": "2_gitlens-file" } @@ -313,11 +319,13 @@ }, { "command": "gitlens.diffWithWorking", + "alt": "gitlens.diffLineWithWorking", "when": "editorTextFocus && config.gitlens.menus.fileDiff.enabled && config.git.enabled", "group": "3_gitlens-file@1.0" }, { "command": "gitlens.diffWithPrevious", + "alt": "gitlens.diffLineWithPrevious", "when": "editorTextFocus && config.gitlens.menus.fileDiff.enabled && config.git.enabled", "group": "3_gitlens-file@1.1" }, @@ -358,7 +366,7 @@ "devDependencies": { "mocha": "^3.1.2", "tslint": "^3.15.1", - "typescript": "^2.0.8", + "typescript": "^2.0.9", "vscode": "^1.0.3", "@types/node": "^6.0.46", "@types/mocha": "^2.2.32", diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index abf3a48..436bb84 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -1,64 +1,184 @@ 'use strict'; -import { Disposable, ExtensionContext, TextEditor, workspace } from 'vscode'; +import { Functions, IDeferred } from './system'; +import { commands, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; import { BlameAnnotationProvider } from './blameAnnotationProvider'; +import { TextDocumentComparer, TextEditorComparer } from './comparers'; +import { BuiltInCommands } from './constants'; import GitProvider from './gitProvider'; +import { Logger } from './logger'; export default class BlameAnnotationController extends Disposable { - private _disposable: Disposable; - private _annotationProvider: BlameAnnotationProvider | undefined; + private _annotationProviders: Map = new Map(); + private _blameAnnotationsDisposable: Disposable; + private _pendingWhitespaceToggleDisposable: Disposable; + private _pendingClearAnnotations: Map void) & IDeferred> = new Map(); + private _pendingWhitespaceToggles: Set = new Set(); + private _visibleColumns: Set; constructor(private context: ExtensionContext, private git: GitProvider) { super(() => this.dispose()); - - const subscriptions: Disposable[] = []; - - // subscriptions.push(window.onDidChangeActiveTextEditor(e => { - // if (!e || !this._controller || this._controller.editor === e) return; - // this.clear(); - // })); - - subscriptions.push(workspace.onDidCloseTextDocument(d => { - if (!this._annotationProvider || this._annotationProvider.uri.fsPath !== d.uri.fsPath) return; - this.clear(); - })); - - this._disposable = Disposable.from(...subscriptions); } dispose() { - this.clear(); - this._disposable && this._disposable.dispose(); + for (const fn of this._pendingClearAnnotations.values()) { + fn.cancel(); + } + this._annotationProviders.forEach(async (p, i) => await this.clear(i)); + + this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); + this._pendingWhitespaceToggleDisposable && this._pendingWhitespaceToggleDisposable.dispose(); } - clear() { - this._annotationProvider && this._annotationProvider.dispose(); - this._annotationProvider = undefined; + async clear(column: number, toggleRenderWhitespace: boolean = true) { + const provider = this._annotationProviders.get(column); + if (!provider) return; + + this._annotationProviders.delete(column); + await provider.dispose(toggleRenderWhitespace); + + if (this._annotationProviders.size === 0) { + Logger.log(`Remove listener registrations for blame annotations`); + this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); + this._blameAnnotationsDisposable = undefined; + } } - get annotated() { - return this._annotationProvider !== undefined; - } + async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { + if (!editor || !editor.document) return false; - showBlameAnnotation(editor: TextEditor, sha?: string): Promise { - if (!editor || !editor.document || editor.document.isUntitled) { - this.clear(); - return Promise.resolve(); + if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) { + Logger.log(`Add listener registrations for blame annotations`); + + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); + subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); + subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); + + this._blameAnnotationsDisposable = Disposable.from(...subscriptions); + + this._visibleColumns = this._getVisibleColumns(window.visibleTextEditors); } - if (!this._annotationProvider) { - this._annotationProvider = new BlameAnnotationProvider(this.context, this.git, editor); - return this._annotationProvider.provideBlameAnnotation(sha); + let provider = this._annotationProviders.get(editor.viewColumn); + if (provider) { + if (TextEditorComparer.equals(provider.editor, editor)) { + await provider.setSelection(shaOrLine); + return true; + } + await this.clear(provider.editor.viewColumn, false); } - return Promise.resolve(); + provider = new BlameAnnotationProvider(this.context, this.git, editor); + this._annotationProviders.set(editor.viewColumn, provider); + return provider.provideBlameAnnotation(shaOrLine); } - toggleBlameAnnotation(editor: TextEditor, sha?: string): Promise { - if (!editor || !editor.document || editor.document.isUntitled || this._annotationProvider) { - this.clear(); - return Promise.resolve(); + async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { + if (!editor || !editor.document) return false; + + let provider = this._annotationProviders.get(editor.viewColumn); + if (!provider) return this.showBlameAnnotation(editor, shaOrLine); + + await this.clear(provider.editor.viewColumn); + return false; + } + + private _getVisibleColumns(editors: TextEditor[]): Set { + const set: Set = new Set(); + for (const e of editors) { + if (e.viewColumn === undefined) continue; + + set.add(e.viewColumn); + } + return set; + } + + private _onActiveTextEditorChanged(e: TextEditor) { + if (e.viewColumn === undefined || this._pendingWhitespaceToggles.size === 0) return; + + if (this._pendingWhitespaceToggles.has(e.viewColumn)) { + Logger.log('ActiveTextEditorChanged:', `Remove pending whitespace toggle for column ${e.viewColumn}`); + this._pendingWhitespaceToggles.delete(e.viewColumn); + + // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace back on + Logger.log('ActiveTextEditorChanged:', `Toggle whitespace rendering on`); + commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); } - return this.showBlameAnnotation(editor, sha); + if (this._pendingWhitespaceToggles.size === 0) { + Logger.log('ActiveTextEditorChanged:', `Remove listener registrations for pending whitespace toggles`); + this._pendingWhitespaceToggleDisposable.dispose(); + this._pendingWhitespaceToggleDisposable = undefined; + } + } + + private _onTextDocumentClosed(e: TextDocument) { + for (const [key, p] of this._annotationProviders) { + if (!TextDocumentComparer.equals(p.document, e)) continue; + + Logger.log('TextDocumentClosed:', `Add pending clear of blame annotations for column ${key}`); + + // Since we don't know if a whole column is going away -- we don't know if we should reset the whitespace + // So defer until onDidChangeVisibleTextEditors fires + const fn = Functions.debounce(() => { + this._pendingClearAnnotations.delete(key); + this.clear(key); + }, 250); + this._pendingClearAnnotations.set(key, fn); + + fn(); + } + } + + private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { + this._visibleColumns = this._getVisibleColumns(window.visibleTextEditors); + + Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${e.viewColumn}`); + await this.clear(e.viewColumn); + + for (const [key, p] of this._annotationProviders) { + if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue; + + Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`); + await this.clear(key, false); + } + } + + private async _onVisibleTextEditorsChanged(e: TextEditor[]) { + if (e.every(_ => _.document.uri.scheme === 'inmemory')) return; + + this._visibleColumns = this._getVisibleColumns(e); + + for (const [key, fn] of this._pendingClearAnnotations) { + Logger.log('VisibleTextEditorsChanged:', `Remove pending blame annotations for column ${key}`); + fn.cancel(); + this._pendingClearAnnotations.delete(key); + + // Clear and reset the whitespace depending on if the column went away + Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`); + await this.clear(key, this._visibleColumns.has(key)); + } + + for (const [key, p] of this._annotationProviders) { + if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue; + + Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`); + const editor = window.activeTextEditor; + if (p.requiresRenderWhitespaceToggle && (editor && editor.viewColumn !== key)) { + this.clear(key, false); + + if (!this._pendingWhitespaceToggleDisposable) { + Logger.log('VisibleTextEditorsChanged:', `Add listener registrations for pending whitespace toggles`); + this._pendingWhitespaceToggleDisposable = window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this); + } + + Logger.log('VisibleTextEditorsChanged:', `Add pending whitespace toggle for column ${key}`); + this._pendingWhitespaceToggles.add(key); + } + else { + this.clear(key); + } + } } } \ No newline at end of file diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index 30b7691..35fe59c 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -1,9 +1,11 @@ 'use strict'; 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 { commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { TextDocumentComparer } from './comparers'; import { BlameAnnotationStyle, IBlameConfig } from './configuration'; -import GitProvider, { GitCommit, IGitBlame } from './gitProvider'; +import { BuiltInCommands } from './constants'; +import GitProvider, { GitCommit, GitUri, IGitBlame } from './gitProvider'; +import { Logger } from './logger'; import * as moment from 'moment'; const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ @@ -15,13 +17,13 @@ const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorat let highlightDecoration: TextEditorDecorationType; export class BlameAnnotationProvider extends Disposable { - public uri: Uri; + public document: TextDocument; + public requiresRenderWhitespaceToggle: boolean = false; private _blame: Promise; private _config: IBlameConfig; private _disposable: Disposable; - private _document: TextDocument; - private _renderWhitespaceSetting: string; + private _uri: GitUri; constructor(context: ExtensionContext, private git: GitProvider, public editor: TextEditor) { super(() => this.dispose()); @@ -44,49 +46,56 @@ export class BlameAnnotationProvider extends Disposable { }); } - this._document = this.editor.document; - this.uri = this._document.uri; - - this._blame = this.git.getBlameForFile(this.uri.fsPath); + this.document = this.editor.document; + this._uri = GitUri.fromUri(this.document.uri); + this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath); this._config = workspace.getConfiguration('gitlens').get('blame'); const subscriptions: Disposable[] = []; + subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); this._disposable = Disposable.from(...subscriptions); + + this._onConfigurationChanged(); } - dispose() { + async dispose(toggleRenderWhitespace: boolean = true) { if (this.editor) { - // HACK: This only works when switching to another editor - diffs handle whitespace toggle differently - if (this._renderWhitespaceSetting !== 'none') { - commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); - } - this.editor.setDecorations(blameDecoration, []); this.editor.setDecorations(highlightDecoration, []); } + // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace back on + if (toggleRenderWhitespace && this.requiresRenderWhitespaceToggle) { + Logger.log('BlameAnnotationProvider.dispose:', `Toggle whitespace rendering on`); + await commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); + } + this._disposable && this._disposable.dispose(); } - private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) { - const blame = await this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line); - if (blame) { - this._applyCommitHighlight(blame.commit.sha); - } + private _onConfigurationChanged() { + const renderWhitespace = workspace.getConfiguration('editor').get('renderWhitespace'); + this.requiresRenderWhitespaceToggle = !(renderWhitespace == null || renderWhitespace === 'none'); } - async provideBlameAnnotation(sha?: string) { + private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) { + if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return; + + return this.setSelection(e.selections[0].active.line); + } + + async provideBlameAnnotation(shaOrLine?: string | number): Promise { const blame = await this._blame; - if (!blame || !blame.lines.length) return; + if (!blame || !blame.lines.length) return false; // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off - this._renderWhitespaceSetting = workspace.getConfiguration('editor').get('renderWhitespace'); - if (this._renderWhitespaceSetting !== 'none') { - commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); + if (this.requiresRenderWhitespaceToggle) { + Logger.log('BlameAnnotationProvider.provideBlameAnnotation:', `Toggle whitespace rendering off`); + await commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace); } let blameDecorationOptions: DecorationOptions[] | undefined; @@ -103,23 +112,49 @@ export class BlameAnnotationProvider extends Disposable { this.editor.setDecorations(blameDecoration, blameDecorationOptions); } - sha = sha || Iterables.first(blame.commits.values()).sha; - - return this._applyCommitHighlight(sha); + this._setSelection(blame, shaOrLine); + return true; } - private async _applyCommitHighlight(sha: string) { + async setSelection(shaOrLine?: string | number) { const blame = await this._blame; if (!blame || !blame.lines.length) return; + return this._setSelection(blame, shaOrLine); + } + + private _setSelection(blame: IGitBlame, shaOrLine?: string | number) { + const offset = this._uri.offset; + + let sha: string; + if (typeof shaOrLine === 'string') { + sha = shaOrLine; + } + else if (typeof shaOrLine === 'number') { + const line = shaOrLine - offset; + if (line >= 0) { + sha = blame.lines[line].sha; + } + } + else { + sha = Iterables.first(blame.commits.values()).sha; + } + + if (!sha) { + this.editor.setDecorations(highlightDecoration, []); + return; + } + const highlightDecorationRanges = blame.lines .filter(l => l.sha === sha) - .map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); + .map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000))); this.editor.setDecorations(highlightDecoration, highlightDecorationRanges); } private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] { + const offset = this._uri.offset; + let count = 0; let lastSha: string; return blame.lines.map(l => { @@ -143,7 +178,7 @@ export class BlameAnnotationProvider extends Disposable { count = -1; } - const isEmptyOrWhitespace = this._document.lineAt(l.line).isEmptyOrWhitespace; + const isEmptyOrWhitespace = this.document.lineAt(l.line).isEmptyOrWhitespace; if (!isEmptyOrWhitespace) { switch (++count) { case 0: @@ -164,7 +199,7 @@ export class BlameAnnotationProvider extends Disposable { lastSha = l.sha; return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 0)), hoverMessage: hoverMessage, renderOptions: { before: { color: color, contentText: gutter, width: '11em' } } }; @@ -172,6 +207,8 @@ export class BlameAnnotationProvider extends Disposable { } private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] { + const offset = this._uri.offset; + let width = 0; if (this._config.annotation.sha) { width += 5; @@ -211,7 +248,7 @@ export class BlameAnnotationProvider extends Disposable { const gutter = this._getGutter(commit); return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 0)), hoverMessage: hoverMessage, renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } } }; diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts index 346da24..fdcbfe7 100644 --- a/src/blameStatusBarController.ts +++ b/src/blameStatusBarController.ts @@ -1,16 +1,21 @@ 'use strict'; import { Objects } from './system'; -import { Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextEditor, window, workspace } from 'vscode'; -import { IConfig, IStatusBarConfig, StatusBarCommand } from './configuration'; +import { Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { TextDocumentComparer } from './comparers'; +import { IConfig, StatusBarCommand } from './configuration'; import { WorkspaceState } from './constants'; -import GitProvider, { IGitBlameLine } from './gitProvider'; +import GitProvider, { GitCommit, GitUri, IGitBlame } from './gitProvider'; import * as moment from 'moment'; export default class BlameStatusBarController extends Disposable { - private _config: IStatusBarConfig; + private _blame: Promise | undefined; + private _config: IConfig; private _disposable: Disposable; - private _statusBarItem: StatusBarItem|null; - private _statusBarDisposable: Disposable|null; + private _document: TextDocument | undefined; + private _statusBarItem: StatusBarItem | undefined; + private _statusBarDisposable: Disposable | undefined; + private _uri: GitUri; + private _useCaching: boolean; constructor(private context: ExtensionContext, private git: GitProvider) { super(() => this.dispose()); @@ -33,7 +38,7 @@ export default class BlameStatusBarController extends Disposable { private _onConfigure() { const config = workspace.getConfiguration('').get('gitlens'); - if (!Objects.areEquivalent(config.statusBar, this._config)) { + if (!Objects.areEquivalent(config.statusBar, this._config && this._config.statusBar)) { this._statusBarDisposable && this._statusBarDisposable.dispose(); this._statusBarItem && this._statusBarItem.dispose(); @@ -47,7 +52,7 @@ export default class BlameStatusBarController extends Disposable { break; case StatusBarCommand.GitViewHistory: if (!this.context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false)) { - config.statusBar.command = StatusBarCommand.BlameExplorer; + config.statusBar.command = StatusBarCommand.ShowBlameHistory; } break; } @@ -55,28 +60,70 @@ export default class BlameStatusBarController extends Disposable { const subscriptions: Disposable[] = []; - subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveSelectionChanged, this)); - subscriptions.push(window.onDidChangeTextEditorSelection(e => this._onActiveSelectionChanged(e.textEditor))); + subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); + subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); this._statusBarDisposable = Disposable.from(...subscriptions); } else { - this._statusBarDisposable = null; - this._statusBarItem = null; + this._statusBarDisposable = undefined; + this._statusBarItem = undefined; } } - this._config = config.statusBar; + this._config = config; + + this._onActiveTextEditorChanged(window.activeTextEditor); } - private async _onActiveSelectionChanged(editor: TextEditor): Promise { - if (!editor || !editor.document || editor.document.isUntitled) { + private async _onActiveTextEditorChanged(e: TextEditor): Promise { + if (!e || !e.document || e.document.isUntitled || e.viewColumn === undefined) { this.clear(); return; } - const blame = await this.git.getBlameForLine(editor.document.uri.fsPath, editor.selection.active.line); - if (blame) { - this.show(blame); + this._document = e.document; + this._uri = GitUri.fromUri(this._document.uri); + const maxLines = this._config.advanced.caching.statusBar.maxLines; + this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || this._document.lineCount <= maxLines); + if (this._useCaching) { + this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath); + } + else { + this._blame = undefined; + } + + return this._showBlame(e.selection.active.line); + } + + private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { + if (!TextDocumentComparer.equals(this._document, e.textEditor && e.textEditor.document)) return; + + return this._showBlame(e.selections[0].active.line); + } + + private async _showBlame(line: number) { + line = line - this._uri.offset; + + let commit: GitCommit; + if (line >= 0) { + if (this._useCaching) { + const blame = await this._blame; + if (!blame || !blame.lines.length) { + this.clear(); + return; + } + + const sha = blame.lines[line].sha; + commit = blame.commits.get(sha); + } + else { + const blameLine = await this.git.getBlameForLine(this._uri.fsPath, line, this._uri.sha, this._uri.repoPath); + commit = blameLine && blameLine.commit; + } + } + + if (commit) { + this.show(commit); } else { this.clear(); @@ -85,20 +132,23 @@ export default class BlameStatusBarController extends Disposable { clear() { this._statusBarItem && this._statusBarItem.hide(); + this._document = undefined; + this._blame = undefined; } - show(blameLine: IGitBlameLine) { - const commit = blameLine.commit; + show(commit: GitCommit) { this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; - //this._statusBarItem.tooltip = [`Last changed by ${commit.author}`, moment(commit.date).format('MMMM Do, YYYY h:MMa'), '', commit.message].join('\n'); - switch (this._config.command) { + switch (this._config.statusBar.command) { case StatusBarCommand.BlameAnnotate: this._statusBarItem.tooltip = 'Toggle Blame Annotations'; break; - case StatusBarCommand.BlameExplorer: + case StatusBarCommand.ShowBlameHistory: this._statusBarItem.tooltip = 'Open Blame History'; break; + case StatusBarCommand.ShowFileHistory: + this._statusBarItem.tooltip = 'Open File History'; + break; case StatusBarCommand.DiffWithPrevious: this._statusBarItem.tooltip = 'Compare to Previous Commit'; break; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index ee4e40f..36d562c 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; -import GitProvider, { GitCommit } from '../gitProvider'; +import GitProvider, { GitCommit, GitUri } from '../gitProvider'; import { Logger } from '../logger'; export default class DiffLineWithPreviousCommand extends EditorCommand { @@ -10,18 +10,23 @@ export default class DiffLineWithPreviousCommand extends EditorCommand { super(Commands.DiffLineWithPrevious); } - async execute(editor: TextEditor, edit: TextEditorEdit): Promise; - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + async execute(editor: TextEditor): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri): Promise; + async execute(editor: TextEditor, edit?: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + 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; - } + const gitUri = GitUri.fromUri(uri); + const blameline = line - gitUri.offset; + if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(uri.fsPath, line); + const blame = await this.git.getBlameForLine(gitUri.fsPath, blameline, gitUri.sha, gitUri.repoPath); 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 @@ -33,7 +38,7 @@ export default class DiffLineWithPreviousCommand extends EditorCommand { 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; + line = blame.line.originalLine + 1 + gitUri.offset; } catch (ex) { Logger.error('[GitLens.DiffWithPreviousLineCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex); @@ -42,7 +47,7 @@ export default class DiffLineWithPreviousCommand extends EditorCommand { } } catch (ex) { - Logger.error('[GitLens.DiffWithPreviousLineCommand]', `getBlameForLine(${line})`, ex); + Logger.error('[GitLens.DiffWithPreviousLineCommand]', `getBlameForLine(${blameline})`, ex); return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index 9d1ca24..64ab9a7 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; -import GitProvider, { GitCommit } from '../gitProvider'; +import GitProvider, { GitCommit, GitUri } from '../gitProvider'; import { Logger } from '../logger'; export default class DiffLineWithWorkingCommand extends EditorCommand { @@ -10,33 +10,38 @@ export default class DiffLineWithWorkingCommand extends EditorCommand { super(Commands.DiffLineWithWorking); } - async execute(editor: TextEditor, edit: TextEditorEdit): Promise; - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + async execute(editor: TextEditor): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri): Promise; + async execute(editor: TextEditor, edit?: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + 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; - } + const gitUri = GitUri.fromUri(uri); + const blameline = line - gitUri.offset; + if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(uri.fsPath, line); + const blame = await this.git.getBlameForLine(gitUri.fsPath, blameline, gitUri.sha, gitUri.repoPath); 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; + line = blame.line.line + 1 + gitUri.offset; } } catch (ex) { - Logger.error('[GitLens.DiffLineWithWorkingCommand]', `getBlameForLine(${line})`, ex); + Logger.error('[GitLens.DiffLineWithWorkingCommand]', `getBlameForLine(${blameline})`, ex); return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } - return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit, line); + return commands.executeCommand(Commands.DiffWithWorking, uri, commit, line); } } diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index c8a1177..b91d9bf 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -3,7 +3,7 @@ import { Iterables } from '../system'; import { commands, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import GitProvider, { GitCommit } from '../gitProvider'; +import GitProvider, { GitCommit, GitUri } from '../gitProvider'; import { Logger } from '../logger'; import * as moment from 'moment'; import * as path from 'path'; @@ -13,29 +13,33 @@ export default class DiffWithPreviousCommand extends EditorCommand { super(Commands.DiffWithPrevious); } - async execute(editor: TextEditor, edit: TextEditorEdit): Promise; + async execute(editor: TextEditor): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri): 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 { + async execute(editor: TextEditor, edit?: TextEditorEdit, uri?: Uri, commit?: GitCommit, rangeOrLine?: Range | number): Promise { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + 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; - } + const gitUri = GitUri.fromUri(uri); try { - const log = await this.git.getLogForFile(uri.fsPath, rangeOrLine); + const log = await this.git.getLogForFile(gitUri.fsPath, rangeOrLine); if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); - commit = commit ? Iterables.find(log.commits.values(), _ => _.sha === commit.sha) : Iterables.first(log.commits.values()); + const sha = (commit && commit.sha) || gitUri.sha; + commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); } catch (ex) { - Logger.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${uri.fsPath})`, ex); + Logger.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${gitUri.fsPath})`, ex); return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index d9ac09c..fb934a8 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -3,7 +3,7 @@ import { Iterables } from '../system'; import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import GitProvider, { GitCommit } from '../gitProvider'; +import GitProvider, { GitCommit, GitUri } from '../gitProvider'; import { Logger } from '../logger'; import * as path from 'path'; @@ -12,31 +12,36 @@ export default class DiffWithWorkingCommand extends EditorCommand { super(Commands.DiffWithWorking); } - async execute(editor: TextEditor, edit: TextEditorEdit): Promise; - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + async execute(editor: TextEditor): Promise; + async execute(editor: TextEditor, edit: TextEditorEdit, uri: Uri): Promise; + async execute(editor: TextEditor, edit?: TextEditorEdit, uri?: Uri, commit?: GitCommit, line?: number): Promise { + if (!(uri instanceof Uri)) { + if (!editor.document) return undefined; + uri = editor.document.uri; + } + 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; - } + const gitUri = GitUri.fromUri(uri); try { - const log = await this.git.getLogForFile(uri.fsPath); + const log = await this.git.getLogForFile(gitUri.fsPath); if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); - commit = Iterables.first(log.commits.values()); + commit = (gitUri.sha && log.commits.get(gitUri.sha)) || Iterables.first(log.commits.values()); } catch (ex) { - Logger.error('[GitLens.DiffWithWorkingCommand]', `getLogForFile(${uri.fsPath})`, ex); + Logger.error('[GitLens.DiffWithWorkingCommand]', `getLogForFile(${gitUri.fsPath})`, ex); return window.showErrorMessage(`Unable to open diff. See output channel for more details`); } } + const gitUri = GitUri.fromUri(uri); + 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)}`); + await commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), gitUri.fileUri(), `${path.basename(commit.uri.fsPath)} (${commit.sha}) ↔ ${path.basename(gitUri.fsPath)}`); return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); } catch (ex) { diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts index edd2c18..933b7de 100644 --- a/src/commands/showBlame.ts +++ b/src/commands/showBlame.ts @@ -3,30 +3,23 @@ import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import BlameAnnotationController from '../blameAnnotationController'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; -import GitProvider from '../gitProvider'; import { Logger } from '../logger'; export default class ShowBlameCommand extends EditorCommand { - constructor(private git: GitProvider, private annotationController: BlameAnnotationController) { + constructor(private annotationController: BlameAnnotationController) { super(Commands.ShowBlame); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) { - if (sha) { - return this.annotationController.toggleBlameAnnotation(editor, sha); - } - - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } - + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string): Promise { try { - const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line); - return this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha); + if (sha) { + return this.annotationController.showBlameAnnotation(editor, sha); + } + + return this.annotationController.showBlameAnnotation(editor, editor.selection.active.line); } catch (ex) { - Logger.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex); + Logger.error('GitLens.ShowBlameCommand', ex); return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); } } diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index d4126ba..f0d129f 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -2,7 +2,7 @@ import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import GitProvider from '../gitProvider'; +import GitProvider, { GitUri } from '../gitProvider'; import { Logger } from '../logger'; export default class ShowBlameHistoryCommand extends EditorCommand { @@ -20,8 +20,10 @@ export default class ShowBlameHistoryCommand extends EditorCommand { position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } + const gitUri = GitUri.fromUri(uri); + try { - const locations = await this.git.getBlameLocations(uri.fsPath, range); + const locations = await this.git.getBlameLocations(gitUri.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); diff --git a/src/commands/showHistory.ts b/src/commands/showFileHistory.ts similarity index 75% rename from src/commands/showHistory.ts rename to src/commands/showFileHistory.ts index 66bee61..72e7b2e 100644 --- a/src/commands/showHistory.ts +++ b/src/commands/showFileHistory.ts @@ -2,12 +2,12 @@ import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { EditorCommand } from './commands'; import { BuiltInCommands, Commands } from '../constants'; -import GitProvider from '../gitProvider'; +import GitProvider, { GitUri } from '../gitProvider'; import { Logger } from '../logger'; -export default class ShowHistoryCommand extends EditorCommand { +export default class ShowFileHistoryCommand extends EditorCommand { constructor(private git: GitProvider) { - super(Commands.ShowHistory); + super(Commands.ShowFileHistory); } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, position?: Position, sha?: string, line?: number) { @@ -19,14 +19,16 @@ export default class ShowHistoryCommand extends EditorCommand { position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } + const gitUri = GitUri.fromUri(uri); + try { - const locations = await this.git.getLogLocations(uri.fsPath, sha, line); + const locations = await this.git.getLogLocations(gitUri.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); + Logger.error('[GitLens.ShowFileHistoryCommand]', 'getLogLocations', ex); return window.showErrorMessage(`Unable to show history. See output channel for more details`); } } diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts index 1c736d3..cb5f933 100644 --- a/src/commands/toggleBlame.ts +++ b/src/commands/toggleBlame.ts @@ -3,30 +3,23 @@ import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import BlameAnnotationController from '../blameAnnotationController'; import { EditorCommand } from './commands'; import { Commands } from '../constants'; -import GitProvider from '../gitProvider'; import { Logger } from '../logger'; export default class ToggleBlameCommand extends EditorCommand { - constructor(private git: GitProvider, private annotationController: BlameAnnotationController) { + constructor(private annotationController: BlameAnnotationController) { super(Commands.ToggleBlame); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) { - if (sha) { - return this.annotationController.toggleBlameAnnotation(editor, sha); - } - - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } - + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string): Promise { try { - const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line); - return this.annotationController.toggleBlameAnnotation(editor, blame && blame.commit.sha); + if (sha) { + return this.annotationController.toggleBlameAnnotation(editor, sha); + } + + return this.annotationController.toggleBlameAnnotation(editor, editor.selection.active.line); } catch (ex) { - Logger.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex); + Logger.error('GitLens.ToggleBlameCommand', ex); return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); } } diff --git a/src/comparers.ts b/src/comparers.ts new file mode 100644 index 0000000..6ec953a --- /dev/null +++ b/src/comparers.ts @@ -0,0 +1,42 @@ +'use strict'; +import { TextDocument, TextEditor, Uri } from 'vscode'; + +abstract class Comparer { + abstract equals(lhs: T, rhs: T): boolean; +} + +class UriComparer extends Comparer { + equals(lhs: Uri, rhs: Uri) { + if (!lhs && !rhs) return true; + if ((lhs && !rhs) || (!lhs && rhs)) return false; + + return lhs.scheme === rhs.scheme && lhs.fsPath === rhs.fsPath; + } +} + +class TextDocumentComparer extends Comparer { + equals(lhs: TextDocument, rhs: TextDocument) { + if (!lhs && !rhs) return true; + if ((lhs && !rhs) || (!lhs && rhs)) return false; + + return uriComparer.equals(lhs.uri, rhs.uri); + } +} + +class TextEditorComparer extends Comparer { + equals(lhs: TextEditor, rhs: TextEditor) { + if (!lhs && !rhs) return true; + if ((lhs && !rhs) || (!lhs && rhs)) return false; + + return textDocumentComparer.equals(lhs.document, rhs.document); + } +} + +const textDocumentComparer = new TextDocumentComparer(); +const textEditorComparer = new TextEditorComparer(); +const uriComparer = new UriComparer(); +export { + textDocumentComparer as TextDocumentComparer, + textEditorComparer as TextEditorComparer, + uriComparer as UriComparer +}; diff --git a/src/configuration.ts b/src/configuration.ts index 3941b6b..8035cfd 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -16,11 +16,11 @@ export interface IBlameConfig { }; } -export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory'; +export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory'; export const CodeLensCommand = { BlameAnnotate: Commands.ToggleBlame as CodeLensCommand, ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand, - ShowHistory: Commands.ShowHistory as CodeLensCommand, + ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand, GitViewHistory: 'git.viewFileHistory' as CodeLensCommand }; @@ -61,10 +61,11 @@ export interface ICodeLensesConfig { authors: ICodeLensConfig; } -export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory'; +export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'git.viewFileHistory'; export const StatusBarCommand = { BlameAnnotate: Commands.ToggleBlame as StatusBarCommand, - BlameExplorer: Commands.ShowBlameHistory as StatusBarCommand, + ShowBlameHistory: Commands.ShowBlameHistory as StatusBarCommand, + ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand, ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand, GitViewHistory: 'git.viewFileHistory' as StatusBarCommand @@ -85,6 +86,9 @@ export const OutputLevel = { export interface IAdvancedConfig { caching: { enabled: boolean; + statusBar: { + maxLines: number; + } }; debug: boolean; git: string; diff --git a/src/constants.ts b/src/constants.ts index 15e2f69..ac8a837 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,7 +14,7 @@ export const BuiltInCommands = { ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands }; -export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | '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.showFileHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; export const Commands = { DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, DiffLineWithPrevious: 'gitlens.diffLineWithPrevious' as Commands, @@ -22,7 +22,7 @@ export const Commands = { DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, ShowBlame: 'gitlens.showBlame' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, - ShowHistory: 'gitlens.showHistory' as Commands, + ShowFileHistory: 'gitlens.showFileHistory' as Commands, ToggleBlame: 'gitlens.toggleBlame' as Commands, ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands }; diff --git a/src/extension.ts b/src/extension.ts index 0f6f18e..898333c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,7 @@ 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'; +import ShowFileHistoryCommand from './commands/showFileHistory'; import ToggleBlameCommand from './commands/toggleBlame'; import ToggleCodeLensCommand from './commands/toggleCodeLens'; @@ -64,10 +64,10 @@ export async function activate(context: ExtensionContext) { 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 ShowBlameCommand(annotationController)); + context.subscriptions.push(new ToggleBlameCommand(annotationController)); context.subscriptions.push(new ShowBlameHistoryCommand(git)); - context.subscriptions.push(new ShowHistoryCommand(git)); + context.subscriptions.push(new ShowFileHistoryCommand(git)); context.subscriptions.push(new ToggleCodeLensCommand(git)); } diff --git a/src/git/enrichers/blameParserEnricher.ts b/src/git/enrichers/blameParserEnricher.ts index 2e69b6b..b5afc4b 100644 --- a/src/git/enrichers/blameParserEnricher.ts +++ b/src/git/enrichers/blameParserEnricher.ts @@ -35,10 +35,10 @@ export class GitBlameParserEnricher implements IGitEnricher { } private _parseEntries(data: string): IBlameEntry[] { - if (!data) return null; + if (!data) return undefined; const lines = data.split('\n'); - if (!lines.length) return null; + if (!lines.length) return undefined; const entries: IBlameEntry[] = []; @@ -107,7 +107,7 @@ export class GitBlameParserEnricher implements IGitEnricher { entry.fileName = lineParts.slice(1).join(' '); entries.push(entry); - entry = null; + entry = undefined; break; default: @@ -120,7 +120,7 @@ export class GitBlameParserEnricher implements IGitEnricher { enrich(data: string, fileName: string): IGitBlame { const entries = this._parseEntries(data); - if (!entries) return null; + if (!entries) return undefined; const authors: Map = new Map(); const commits: Map = new Map(); diff --git a/src/git/enrichers/logParserEnricher.ts b/src/git/enrichers/logParserEnricher.ts index 2a426f6..81e953b 100644 --- a/src/git/enrichers/logParserEnricher.ts +++ b/src/git/enrichers/logParserEnricher.ts @@ -19,10 +19,10 @@ interface ILogEntry { export class GitLogParserEnricher implements IGitEnricher { private _parseEntries(data: string): ILogEntry[] { - if (!data) return null; + if (!data) return undefined; const lines = data.split('\n'); - if (!lines.length) return null; + if (!lines.length) return undefined; const entries: ILogEntry[] = []; @@ -49,7 +49,7 @@ export class GitLogParserEnricher implements IGitEnricher { break; case 'author-date': - entry.authorDate = lineParts.slice(1).join(' ').trim(); + entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`; break; // case 'committer': @@ -76,7 +76,7 @@ export class GitLogParserEnricher implements IGitEnricher { } entries.push(entry); - entry = null; + entry = undefined; break; default: @@ -89,7 +89,7 @@ export class GitLogParserEnricher implements IGitEnricher { enrich(data: string, fileName: string): IGitLog { const entries = this._parseEntries(data); - if (!entries) return null; + if (!entries) return undefined; const authors: Map = new Map(); const commits: Map = new Map(); diff --git a/src/git/git.ts b/src/git/git.ts index 26958fb..01c23e0 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -83,13 +83,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`, `--date=iso8601-strict`, `--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}`); + return gitCommand(root, 'log', `--name-only`, `--no-merges`, `--date=iso8601-strict`, `--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) { @@ -102,7 +102,7 @@ export default class Git { return; } - //Logger.log(`getVersionedFile(${fileName}, ${sha}); destination=${destination}`); + Logger.log(`getVersionedFile(${fileName}, ${repoPath}, ${sha}); destination=${destination}`); fs.appendFile(destination, data, err => { if (err) { reject(err); diff --git a/src/git/gitLocator.ts b/src/git/gitLocator.ts index e4c59b7..ad49c87 100644 --- a/src/git/gitLocator.ts +++ b/src/git/gitLocator.ts @@ -1,3 +1,4 @@ +'use strict'; import { spawnPromise } from 'spawn-rx'; import * as path from 'path'; diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts index 77b10b9..5c59868 100644 --- a/src/gitBlameCodeLensProvider.ts +++ b/src/gitBlameCodeLensProvider.ts @@ -61,7 +61,7 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider { resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token); if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token); - return Promise.reject(null); + return Promise.reject(undefined); } _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable { diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index d87cc24..1a9a1b4 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -41,7 +41,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language.toLowerCase() === document.languageId); if (languageLocations == null) { languageLocations = { - language: null, + language: undefined, location: this._config.codeLens.location, customSymbols: this._config.codeLens.locationCustomSymbols }; @@ -171,7 +171,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token); if (lens instanceof GitAuthorsCodeLens) return this._resolveGitAuthorsCodeLens(lens, token); - return Promise.reject(null); + return Promise.reject(undefined); } async _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Promise { @@ -186,7 +186,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { 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.ShowFileHistory: return this._applyShowFileHistoryCommand(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; @@ -202,7 +202,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { 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.ShowFileHistory: return this._applyShowFileHistoryCommand(title, lens, blame); case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); default: return lens; @@ -227,7 +227,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowHistoryCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit) { + _applyShowFileHistoryCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit) { let line = lens.range.start.line; const blameLine = commit.lines.find(_ => _.line === line); if (blameLine) { @@ -237,7 +237,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { const position = lens.isFullRange ? new Position(1, 0) : lens.range.start; lens.command = { title: title, - command: Commands.ShowHistory, + command: Commands.ShowFileHistory, arguments: [Uri.file(lens.fileName), position, commit.sha, line] }; return lens; @@ -262,7 +262,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { } _applyGitHistoryCommand(title: string, lens: T, blame: IGitBlameLines) { - if (!this._hasGitHistoryExtension) return this._applyShowHistoryCommand(title, lens, blame); + if (!this._hasGitHistoryExtension) return this._applyShowFileHistoryCommand(title, lens, blame); lens.command = { title: title, diff --git a/src/gitContentProvider.ts b/src/gitContentProvider.ts index 3af63a2..4b39f46 100644 --- a/src/gitContentProvider.ts +++ b/src/gitContentProvider.ts @@ -1,18 +1,29 @@ 'use strict'; -import { ExtensionContext, TextDocumentContentProvider, Uri } from 'vscode'; +import { ExtensionContext, TextDocumentContentProvider, Uri, window } from 'vscode'; import { DocumentSchemes } from './constants'; import GitProvider from './gitProvider'; import { Logger } from './logger'; +import * as path from 'path'; export default class GitContentProvider implements TextDocumentContentProvider { static scheme = DocumentSchemes.Git; constructor(context: ExtensionContext, private git: GitProvider) { } - provideTextDocumentContent(uri: Uri): string | Thenable { + async provideTextDocumentContent(uri: Uri): Promise { const data = GitProvider.fromGitUri(uri); - return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.repoPath, data.sha) - .then(text => data.decoration ? `${data.decoration}\n${text}` : text) - .catch(ex => Logger.error('[GitLens.GitContentProvider]', 'getVersionedFileText', ex)); + const fileName = data.originalFileName || data.fileName; + try { + let text = await this.git.getVersionedFileText(fileName, data.repoPath, data.sha); + if (data.decoration) { + text = `${data.decoration}\n${text}`; + } + return text; + } + catch (ex) { + Logger.error('[GitLens.GitContentProvider]', 'getVersionedFileText', ex); + await window.showErrorMessage(`Unable to show Git revision ${data.sha} of '${path.relative(data.repoPath, fileName)}'`); + return undefined; + } } } \ No newline at end of file diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 0f3cd10..4301f3f 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -39,16 +39,16 @@ enum RemoveCacheReason { } export default class GitProvider extends Disposable { - private _cache: Map | null; - private _cacheDisposable: Disposable | null; + private _cache: Map | undefined; + private _cacheDisposable: Disposable | undefined; private _config: IConfig; private _disposable: Disposable; - private _codeLensProviderDisposable: Disposable | null; + private _codeLensProviderDisposable: Disposable | undefined; private _codeLensProviderSelector: DocumentFilter; private _gitignore: Promise; - static EmptyPromise: Promise = Promise.resolve(null); + static EmptyPromise: Promise = Promise.resolve(undefined); static BlameFormat = GitBlameFormat.incremental; constructor(private context: ExtensionContext) { @@ -58,7 +58,7 @@ export default class GitProvider extends Disposable { this._onConfigure(); - this._gitignore = new Promise((resolve, reject) => { + this._gitignore = new Promise((resolve, reject) => { const gitignorePath = path.join(repoPath, '.gitignore'); fs.exists(gitignorePath, e => { if (e) { @@ -67,11 +67,11 @@ export default class GitProvider extends Disposable { resolve(ignore().add(data)); return; } - resolve(null); + resolve(undefined); }); return; } - resolve(null); + resolve(undefined); }); }); @@ -106,7 +106,7 @@ export default class GitProvider extends Disposable { this._codeLensProviderSelector = GitCodeLensProvider.selector; this._codeLensProviderDisposable = languages.registerCodeLensProvider(this._codeLensProviderSelector, new GitCodeLensProvider(this.context, this)); } else { - this._codeLensProviderDisposable = null; + this._codeLensProviderDisposable = undefined; } } @@ -127,9 +127,9 @@ export default class GitProvider extends Disposable { this._cacheDisposable = Disposable.from(...disposables); } else { this._cacheDisposable && this._cacheDisposable.dispose(); - this._cacheDisposable = null; + this._cacheDisposable = undefined; this._cache && this._cache.clear(); - this._cache = null; + this._cache = undefined; } } @@ -167,13 +167,15 @@ export default class GitProvider extends Disposable { return Git.repoPath(cwd); } - getBlameForFile(fileName: string): Promise { - Logger.log(`getBlameForFile('${fileName}')`); + getBlameForFile(fileName: string, sha?: string, repoPath?: string): Promise { + Logger.log(`getBlameForFile('${fileName}', ${sha}, ${repoPath})`); fileName = Git.normalizePath(fileName); + const useCaching = this.UseCaching && !sha; + let cacheKey: string | undefined; let entry: CacheEntry | undefined; - if (this.UseCaching) { + if (useCaching) { cacheKey = this._getCacheEntryKey(fileName); entry = this._cache.get(cacheKey); @@ -189,11 +191,11 @@ export default class GitProvider extends Disposable { return >GitProvider.EmptyPromise; } - return Git.blame(GitProvider.BlameFormat, fileName) + return Git.blame(GitProvider.BlameFormat, fileName, sha, repoPath) .then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName)) .catch(ex => { // Trap and cache expected blame errors - if (this.UseCaching) { + if (useCaching) { const msg = ex && ex.toString(); Logger.log(`Replace blame cache with empty promise for '${cacheKey}'`); @@ -206,11 +208,11 @@ export default class GitProvider extends Disposable { this._cache.set(cacheKey, entry); return >GitProvider.EmptyPromise; } - return null; + return undefined; }); }); - if (this.UseCaching) { + if (useCaching) { Logger.log(`Add blame cache for '${cacheKey}'`); entry.blame = { @@ -224,13 +226,13 @@ export default class GitProvider extends Disposable { return promise; } - async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { + async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { Logger.log(`getBlameForLine('${fileName}', ${line}, ${sha}, ${repoPath})`); if (this.UseCaching && !sha) { const blame = await this.getBlameForFile(fileName); const blameLine = blame && blame.lines[line]; - if (!blameLine) return null; + if (!blameLine) return undefined; const commit = blame.commits.get(blameLine.sha); return { @@ -245,7 +247,7 @@ export default class GitProvider extends Disposable { try { const data = await Git.blameLines(GitProvider.BlameFormat, fileName, line + 1, line + 1, sha, repoPath); const blame = new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName); - if (!blame) return null; + if (!blame) return undefined; const commit = Iterables.first(blame.commits.values()); if (repoPath) { @@ -258,15 +260,15 @@ export default class GitProvider extends Disposable { }; } catch (ex) { - return null; + return undefined; } } - async getBlameForRange(fileName: string, range: Range): Promise { + async getBlameForRange(fileName: string, range: Range): Promise { Logger.log(`getBlameForRange('${fileName}', ${range})`); const blame = await this.getBlameForFile(fileName); - if (!blame) return null; + if (!blame) return undefined; if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); @@ -312,11 +314,11 @@ export default class GitProvider extends Disposable { }; } - async getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { + async getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { Logger.log(`getBlameForShaRange('${fileName}', ${sha}, ${range})`); const blame = await this.getBlameForFile(fileName); - if (!blame) return null; + if (!blame) return undefined; const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha); let commit = blame.commits.get(sha); @@ -329,11 +331,11 @@ export default class GitProvider extends Disposable { }; } - async getBlameLocations(fileName: string, range: Range): Promise { + async getBlameLocations(fileName: string, range: Range): Promise { Logger.log(`getBlameForShaRange('${fileName}', ${range})`); const blame = await this.getBlameForRange(fileName, range); - if (!blame) return null; + if (!blame) return undefined; const commitCount = blame.commits.size; @@ -351,7 +353,7 @@ export default class GitProvider extends Disposable { return locations; } - getLogForFile(fileName: string, range?: Range): Promise { + getLogForFile(fileName: string, range?: Range): Promise { Logger.log(`getLogForFile('${fileName}', ${range})`); fileName = Git.normalizePath(fileName); @@ -392,7 +394,7 @@ export default class GitProvider extends Disposable { this._cache.set(cacheKey, entry); return >GitProvider.EmptyPromise; } - return null; + return undefined; }); }); @@ -410,11 +412,11 @@ export default class GitProvider extends Disposable { return promise; } - async getLogLocations(fileName: string, sha?: string, line?: number): Promise { + 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; + if (!log) return undefined; const commitCount = log.commits.size; @@ -454,7 +456,7 @@ export default class GitProvider extends Disposable { this._codeLensProviderDisposable.dispose(); if (editor.document.fileName === (this._codeLensProviderSelector && this._codeLensProviderSelector.pattern)) { - this._codeLensProviderDisposable = null; + this._codeLensProviderDisposable = undefined; return; } } @@ -468,7 +470,7 @@ export default class GitProvider extends Disposable { disposables.push(window.onDidChangeActiveTextEditor(e => { if (e.viewColumn && e.document !== editor.document) { this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = null; + this._codeLensProviderDisposable = undefined; } })); @@ -487,7 +489,7 @@ export default class GitProvider extends Disposable { return data; } - static fromGitUri(uri: Uri) { + static fromGitUri(uri: Uri): IGitUriData { if (uri.scheme !== DocumentSchemes.Git) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); return GitProvider._fromGitUri(uri); } @@ -538,6 +540,42 @@ export default class GitProvider extends Disposable { } } +export class GitUri extends Uri { + offset: number; + repoPath?: string | undefined; + sha?: string | undefined; + + constructor(uri?: Uri) { + super(); + if (!uri) return; + + const base = this; + base._scheme = uri.scheme; + base._authority = uri.authority; + base._path = uri.path; + base._query = uri.query; + base._fragment = uri.fragment; + + this.offset = 0; + if (uri.scheme === DocumentSchemes.Git || uri.scheme === DocumentSchemes.GitBlame) { + const data = GitProvider.fromGitUri(uri); + base._fsPath = data.originalFileName || data.fileName; + + this.offset = (data.decoration && data.decoration.split('\n').length) || 0; + this.repoPath = data.repoPath; + this.sha = data.sha; + } + } + + fileUri() { + return Uri.file(this.fsPath); + } + + static fromUri(uri: Uri) { + return new GitUri(uri); + } +} + export interface IGitUriData { repoPath: string; fileName: string; diff --git a/src/system.ts b/src/system.ts index f4c94f2..767b840 100644 --- a/src/system.ts +++ b/src/system.ts @@ -1,3 +1,4 @@ +'use strict'; // export * from './system/array'; // export * from './system/disposable'; // export * from './system/element'; diff --git a/src/system/iterable.ts b/src/system/iterable.ts index 4f11259..207752f 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -58,4 +58,11 @@ export namespace Iterables { export function next(source: IterableIterator): T { return source.next().value; } + + export function some(source: Iterable | IterableIterator, predicate: (item: T) => boolean): boolean { + for (const item of source) { + if (predicate(item)) return true; + } + return false; + } } \ No newline at end of file