diff --git a/src/blameActiveLineController.ts b/src/blameActiveLineController.ts index 1fca9e8..0f40dae 100644 --- a/src/blameActiveLineController.ts +++ b/src/blameActiveLineController.ts @@ -6,7 +6,7 @@ import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotati import { TextEditorComparer } from './comparers'; import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; -import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitBlame, IGitCommitLine } from './gitService'; +import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; import * as moment from 'moment'; const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ @@ -18,7 +18,6 @@ const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDe export class BlameActiveLineController extends Disposable { private _activeEditorLineDisposable: Disposable | undefined; - private _blame: Promise | undefined; private _blameable: boolean; private _config: IConfig; private _currentLine: number = -1; @@ -27,7 +26,6 @@ export class BlameActiveLineController extends Disposable { private _statusBarItem: StatusBarItem | undefined; private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise; private _uri: GitUri; - private _useCaching: boolean; constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: BlameAnnotationController) { super(() => this.dispose()); @@ -135,13 +133,11 @@ export class BlameActiveLineController extends Disposable { this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; this._editor = editor; this._uri = await GitUri.fromUri(editor.document.uri, this.git); + const maxLines = this._config.advanced.caching.statusBar.maxLines; - this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines); - if (this._useCaching) { - this._blame = this.git.getBlameForFile(this._uri); - } - else { - this._blame = undefined; + // If caching is on and the file is small enough -- kick off a blame for the whole file + if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { + this.git.getBlameForFile(this._uri); } this._updateBlame(editor.selection.active.line, editor); @@ -165,7 +161,6 @@ export class BlameActiveLineController extends Disposable { } private _onGitCacheChanged() { - this._blame = undefined; this._onActiveTextEditorChanged(window.activeTextEditor); } @@ -191,22 +186,9 @@ export class BlameActiveLineController extends Disposable { let commitLine: IGitCommitLine | undefined = undefined; // Since blame information isn't valid when there are unsaved changes -- don't show any status if (this._blameable && line >= 0) { - if (this._useCaching) { - const blame = this._blame && await this._blame; - if (blame === undefined || !blame.lines.length) { - this.clear(editor); - return; - } - - commitLine = blame.lines[line]; - const sha = commitLine === undefined ? undefined : commitLine.sha; - commit = sha === undefined ? undefined : blame.commits.get(sha); - } - else { - const blameLine = await this.git.getBlameForLine(this._uri, line); - commitLine = blameLine === undefined ? undefined : blameLine.line; - commit = blameLine === undefined ? undefined : blameLine.commit; - } + const blameLine = await this.git.getBlameForLine(this._uri, line); + commitLine = blameLine === undefined ? undefined : blameLine.line; + commit = blameLine === undefined ? undefined : blameLine.commit; } if (commit !== undefined && commitLine !== undefined) { diff --git a/src/gitService.ts b/src/gitService.ts index 5e1852b..dd0115c 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -25,15 +25,21 @@ class UriCacheEntry { class GitCacheEntry { - blame?: ICachedBlame; - log?: ICachedLog; + private cache: Map = new Map(); + + constructor(public key: string) { } get hasErrors(): boolean { - return (this.blame !== undefined && this.blame.errorMessage !== undefined) || - (this.log !== undefined && this.log.errorMessage !== undefined); + return Iterables.every(this.cache.values(), _ => _.errorMessage !== undefined); } - constructor(public key: string) { } + get (key: string): T | undefined { + return this.cache.get(key) as T; + } + + set (key: string, value: T) { + this.cache.set(key, value); + } } interface ICachedItem { @@ -312,38 +318,65 @@ export class GitService extends Disposable { } async getBlameForFile(uri: GitUri): Promise { - Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + let key: string = 'blame'; + if (uri.sha !== undefined) { + key += `:${uri.sha}`; + } const fileName = uri.fsPath; let entry: GitCacheEntry | undefined; - if (this.UseCaching && !uri.sha) { + if (this.UseCaching) { const cacheKey = this.getCacheEntryKey(fileName); entry = this._gitCache.get(cacheKey); - if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; + if (entry !== undefined) { + const cachedBlame = entry.get(key); + if (cachedBlame !== undefined) { + Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + return cachedBlame.item; + } + + if (key !== 'blame') { + // Since we are looking for partial blame, see if we have the blame of the whole file + const cachedBlame = entry.get('blame'); + if (cachedBlame !== undefined) { + Logger.log(`? Cache(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + const blame = await cachedBlame.item; + if (blame !== undefined && blame.commits.has(uri.sha!)) { + Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + return cachedBlame.item; + } + } + } + } + + Logger.log(`Not Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + if (entry === undefined) { entry = new GitCacheEntry(cacheKey); + this._gitCache.set(entry.key, entry); } } + else { + Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + } - const promise = this._getBlameForFile(uri, fileName, entry); + const promise = this._getBlameForFile(uri, fileName, entry, key); if (entry) { - Logger.log(`Add blame cache for '${entry.key}'`); + Logger.log(`Add blame cache for '${entry.key}:${key}'`); - entry.blame = { + entry.set(key, { //date: new Date(), item: promise - } as ICachedBlame; - - this._gitCache.set(entry.key, entry); + } as ICachedBlame); } return promise; } - private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined): Promise { + private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined, key: string): Promise { const [file, root] = Git.splitPath(fileName, uri.repoPath, false); const ignore = await this._gitignore; @@ -363,16 +396,15 @@ export class GitService extends Disposable { // Trap and cache expected blame errors if (entry) { const msg = ex && ex.toString(); - Logger.log(`Replace blame cache with empty promise for '${entry.key}'`); + Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`); - entry.blame = { + entry.set(key, { //date: new Date(), item: GitService.EmptyPromise, errorMessage: msg - } as ICachedBlame; + } as ICachedBlame); this._onDidBlameFail.fire(entry.key); - this._gitCache.set(entry.key, entry); return await GitService.EmptyPromise as IGitBlame; } @@ -383,7 +415,7 @@ export class GitService extends Disposable { async getBlameForLine(uri: GitUri, line: number): Promise { Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`); - if (this.UseCaching && !uri.sha) { + if (this.UseCaching) { const blame = await this.getBlameForFile(uri); if (blame === undefined) return undefined; @@ -604,37 +636,72 @@ export class GitService extends Disposable { } } - getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise { - Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`); + async getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise { + let key: string = 'log'; + if (sha !== undefined) { + key += `:${sha}`; + } + if (maxCount !== undefined) { + key += `:n${maxCount}`; + } let entry: GitCacheEntry | undefined; - if (this.UseCaching && !sha && !range && !maxCount && !reverse) { + if (this.UseCaching && range === undefined && !reverse) { const cacheKey = this.getCacheEntryKey(fileName); entry = this._gitCache.get(cacheKey); - if (entry !== undefined && entry.log !== undefined) return entry.log.item; + if (entry !== undefined) { + const cachedLog = entry.get(key); + if (cachedLog !== undefined) { + Logger.log(`Cached(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); + return cachedLog.item; + } + + if (key !== 'log') { + // Since we are looking for partial log, see if we have the log of the whole file + const cachedLog = entry.get('log'); + if (cachedLog !== undefined) { + if (sha === undefined) { + Logger.log(`Cached(~${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); + return cachedLog.item; + } + + Logger.log(`? Cache(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); + const log = await cachedLog.item; + if (log !== undefined && log.commits.has(sha)) { + Logger.log(`Cached(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); + return cachedLog.item; + } + } + } + } + + Logger.log(`Not Cached(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); + if (entry === undefined) { entry = new GitCacheEntry(cacheKey); + this._gitCache.set(entry.key, entry); } } + else { + Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`); + } - const promise = this._getLogForFile(repoPath, fileName, sha, range, maxCount, reverse, entry); + const promise = this._getLogForFile(repoPath, fileName, sha, range, maxCount, reverse, entry, key); if (entry) { - Logger.log(`Add log cache for '${entry.key}'`); + Logger.log(`Add log cache for '${entry.key}:${key}'`); - entry.log = { + entry.set(key, { //date: new Date(), item: promise - } as ICachedLog; - - this._gitCache.set(entry.key, entry); + } as ICachedLog); } return promise; } - private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined): Promise { + private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined, key: string): Promise { const [file, root] = Git.splitPath(fileName, repoPath, false); const ignore = await this._gitignore; @@ -651,15 +718,14 @@ export class GitService extends Disposable { // Trap and cache expected log errors if (entry) { const msg = ex && ex.toString(); - Logger.log(`Replace log cache with empty promise for '${entry.key}'`); + Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`); - entry.log = { + entry.set(key, { //date: new Date(), item: GitService.EmptyPromise, errorMessage: msg - } as ICachedLog; + } as ICachedLog); - this._gitCache.set(entry.key, entry); return await GitService.EmptyPromise as IGitLog; }