From 2e93ebce771522202158ee335d2c36d10ce086ea Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:30:33 +0100 Subject: [PATCH] Git - extract editor decoration (#234292) --- extensions/git/src/blame.ts | 203 ++++++++++++++++++++---------------- 1 file changed, 116 insertions(+), 87 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 3a9a6513845..73d1a9c4fec 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConfigurationChangeEvent, DecorationOptions, l10n, Position, Range, TextDocument, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, IDisposable, pathEquals } from './util'; +import { dispose, fromNow, IDisposable, pathEquals, runAndSubscribeEvent } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; @@ -96,42 +96,31 @@ interface ResourceBlameInformation { readonly blameInformation: BlameInformation[]; } -export class GitBlameController { - private readonly _decorationType: TextEditorDecorationType; +interface LineBlameInformation { + readonly lineNumber: number; + readonly blameInformation: BlameInformation | string; +} - private readonly _blameInformation = new Map(); +export class GitBlameController { + private readonly _onDidChangeBlameInformation = new EventEmitter(); + public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; + + readonly textEditorBlameInformation = new Map(); + private readonly _repositoryBlameInformation = new Map(); private _repositoryDisposables = new Map(); private _disposables: IDisposable[] = []; constructor(private readonly _model: Model) { - this._decorationType = window.createTextEditorDecorationType({ - isWholeLine: true, - after: { - color: new ThemeColor('git.blame.editorDecorationForeground') - } - }); - this._disposables.push(this._decorationType); + this._disposables.push(new GitBlameEditorDecoration(this)); this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._disposables); this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._disposables); - workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); + window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._disposables); + window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._disposables); - window.onDidChangeTextEditorSelection(e => this._updateDecorations(e.textEditor), this, this._disposables); - window.onDidChangeTextEditorDiffInformation(e => this._updateDecorations(e.textEditor), this, this._disposables); - - this._updateDecorations(window.activeTextEditor); - } - - private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.editorDecoration.enabled')) { - return; - } - - for (const textEditor of window.visibleTextEditors) { - this._updateDecorations(textEditor); - } + this._updateTextEditorBlameInformation(window.activeTextEditor); } private _onDidOpenRepository(repository: Repository): void { @@ -148,11 +137,11 @@ export class GitBlameController { } this._repositoryDisposables.delete(repository); - this._blameInformation.delete(repository); + this._repositoryBlameInformation.delete(repository); } private _onDidRunGitStatus(repository: Repository): void { - let repositoryBlameInformation = this._blameInformation.get(repository); + let repositoryBlameInformation = this._repositoryBlameInformation.get(repository); if (!repositoryBlameInformation) { return; } @@ -161,12 +150,12 @@ export class GitBlameController { // 1. HEAD commit changed (remove all blame information for the repository) if (repositoryBlameInformation.commit !== repository.HEAD?.commit) { - this._blameInformation.delete(repository); + this._repositoryBlameInformation.delete(repository); repositoryBlameInformation = undefined; updateDecorations = true; } - // 2. Resource has been staged/unstaged (remove blame information for the file) + // 2. Resource has been staged/unstaged (remove blame information for the resource) for (const [uri, resourceBlameInformation] of repositoryBlameInformation?.blameInformation.entries() ?? []) { const isStaged = repository.indexGroup.resourceStates .some(r => pathEquals(uri.fsPath, r.resourceUri.fsPath)); @@ -179,32 +168,49 @@ export class GitBlameController { if (updateDecorations) { for (const textEditor of window.visibleTextEditors) { - this._updateDecorations(textEditor); + this._updateTextEditorBlameInformation(textEditor); } } } + private async _getBlameInformation(resource: Uri): Promise { + const repository = this._model.getRepository(resource); + if (!repository || !repository.HEAD?.commit) { + return undefined; + } + + const repositoryBlameInformation = this._repositoryBlameInformation.get(repository) ?? { + commit: repository.HEAD.commit, + blameInformation: new Map() + } satisfies RepositoryBlameInformation; + + let resourceBlameInformation = repositoryBlameInformation.blameInformation.get(resource); + if (repositoryBlameInformation.commit === repository.HEAD.commit && resourceBlameInformation) { + return resourceBlameInformation.blameInformation; + } + + const staged = repository.indexGroup.resourceStates + .some(r => pathEquals(resource.fsPath, r.resourceUri.fsPath)); + const blameInformation = await repository.blame2(resource.fsPath) ?? []; + resourceBlameInformation = { staged, blameInformation } satisfies ResourceBlameInformation; + + this._repositoryBlameInformation.set(repository, { + ...repositoryBlameInformation, + blameInformation: repositoryBlameInformation.blameInformation.set(resource, resourceBlameInformation) + }); + + return resourceBlameInformation.blameInformation; + } + @throttle - private async _updateDecorations(textEditor: TextEditor | undefined): Promise { - if (!textEditor) { - return; - } - - const enabled = workspace.getConfiguration('git').get('blame.editorDecoration.enabled', false); - if (!enabled) { - textEditor.setDecorations(this._decorationType, []); - return; - } - - const diffInformation = textEditor.diffInformation; + private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise { + const diffInformation = textEditor?.diffInformation; if (!diffInformation || diffInformation.isStale) { - textEditor.setDecorations(this._decorationType, []); return; } - const resourceBlameInformation = await this._getBlameInformation(textEditor.document); + const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri); if (!resourceBlameInformation) { - textEditor.setDecorations(this._decorationType, []); return; } @@ -216,11 +222,11 @@ export class GitBlameController { resourceBlameInformation, diffInformation.changes); - const decorations: DecorationOptions[] = []; + const lineBlameInformation: LineBlameInformation[] = []; for (const lineNumber of textEditor.selections.map(s => s.active.line)) { // Check if the line is contained in the diff information if (isLineChanged(lineNumber + 1, diffInformation.changes)) { - decorations.push(this._createDecoration(lineNumber, l10n.t('Not Committed Yet'))); + lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); continue; } @@ -233,15 +239,72 @@ export class GitBlameController { }); if (blameInformation) { - if (blameInformation.id === notCommittedYetId) { - decorations.push(this._createDecoration(lineNumber, l10n.t('Not Committed Yet (Staged)'))); + if (blameInformation.id !== notCommittedYetId) { + lineBlameInformation.push({ lineNumber, blameInformation }); } else { - const ago = fromNow(blameInformation.date ?? Date.now(), true, true); - decorations.push(this._createDecoration(lineNumber, `${blameInformation.message ?? ''}, ${blameInformation.authorName ?? ''} (${ago})`)); + lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') }); } } } + this.textEditorBlameInformation.set(textEditor, lineBlameInformation); + this._onDidChangeBlameInformation.fire(textEditor); + } + + dispose() { + for (const disposables of this._repositoryDisposables.values()) { + dispose(disposables); + } + this._repositoryDisposables.clear(); + + this._disposables = dispose(this._disposables); + } +} + +class GitBlameEditorDecoration { + private readonly _decorationType: TextEditorDecorationType; + private _disposables: IDisposable[] = []; + + constructor(private readonly _controller: GitBlameController) { + this._decorationType = window.createTextEditorDecorationType({ + isWholeLine: true, + after: { + color: new ThemeColor('git.blame.editorDecorationForeground') + } + }); + this._disposables.push(this._decorationType); + + this._disposables.push(runAndSubscribeEvent(workspace.onDidChangeConfiguration, e => { + if (!e || e?.affectsConfiguration('git.blame.editorDecoration.enabled')) { + for (const textEditor of window.visibleTextEditors) { + this._updateDecorations(textEditor); + } + } + })); + + this._controller.onDidChangeBlameInformation(e => this._updateDecorations(e), this, this._disposables); + } + + private _updateDecorations(textEditor: TextEditor): void { + const enabled = workspace.getConfiguration('git').get('blame.editorDecoration.enabled', false); + if (!enabled) { + textEditor.setDecorations(this._decorationType, []); + return; + } + + const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); + if (!blameInformation) { + textEditor.setDecorations(this._decorationType, []); + return; + } + + const decorations = blameInformation.map(blame => { + const contentText = typeof blame.blameInformation === 'string' + ? blame.blameInformation + : `${blame.blameInformation.message ?? ''}, ${blame.blameInformation.authorName ?? ''} (${fromNow(blame.blameInformation.date ?? Date.now(), true, true)})`; + return this._createDecoration(blame.lineNumber, contentText); + }); + textEditor.setDecorations(this._decorationType, decorations); } @@ -258,41 +321,7 @@ export class GitBlameController { }; } - private async _getBlameInformation(document: TextDocument): Promise { - const repository = this._model.getRepository(document.uri); - if (!repository || !repository.HEAD?.commit) { - return undefined; - } - - const repositoryBlameInformation = this._blameInformation.get(repository) ?? { - commit: repository.HEAD.commit, - blameInformation: new Map() - } satisfies RepositoryBlameInformation; - - let resourceBlameInformation = repositoryBlameInformation.blameInformation.get(document.uri); - if (repositoryBlameInformation.commit === repository.HEAD.commit && resourceBlameInformation) { - return resourceBlameInformation.blameInformation; - } - - const staged = repository.indexGroup.resourceStates - .some(r => pathEquals(document.uri.fsPath, r.resourceUri.fsPath)); - const blameInformation = await repository.blame2(document.uri.fsPath) ?? []; - resourceBlameInformation = { staged, blameInformation } satisfies ResourceBlameInformation; - - this._blameInformation.set(repository, { - ...repositoryBlameInformation, - blameInformation: repositoryBlameInformation.blameInformation.set(document.uri, resourceBlameInformation) - }); - - return resourceBlameInformation.blameInformation; - } - dispose() { - for (const disposables of this._repositoryDisposables.values()) { - dispose(disposables); - } - this._repositoryDisposables.clear(); - this._disposables = dispose(this._disposables); } }