Git - extract editor decoration (#234292)
This commit is contained in:
Родитель
b958720bd5
Коммит
2e93ebce77
|
@ -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<Repository, RepositoryBlameInformation>();
|
||||
export class GitBlameController {
|
||||
private readonly _onDidChangeBlameInformation = new EventEmitter<TextEditor>();
|
||||
public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event;
|
||||
|
||||
readonly textEditorBlameInformation = new Map<TextEditor, readonly LineBlameInformation[]>();
|
||||
private readonly _repositoryBlameInformation = new Map<Repository, RepositoryBlameInformation>();
|
||||
|
||||
private _repositoryDisposables = new Map<Repository, IDisposable[]>();
|
||||
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<BlameInformation[] | undefined> {
|
||||
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<Uri, ResourceBlameInformation>()
|
||||
} 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<void> {
|
||||
if (!textEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = workspace.getConfiguration('git').get<boolean>('blame.editorDecoration.enabled', false);
|
||||
if (!enabled) {
|
||||
textEditor.setDecorations(this._decorationType, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const diffInformation = textEditor.diffInformation;
|
||||
private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise<void> {
|
||||
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<boolean>('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<BlameInformation[] | undefined> {
|
||||
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<Uri, ResourceBlameInformation>()
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче