This commit is contained in:
Ladislau Szomoru 2024-11-20 20:30:33 +01:00 коммит произвёл GitHub
Родитель b958720bd5
Коммит 2e93ebce77
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
1 изменённых файлов: 116 добавлений и 87 удалений

Просмотреть файл

@ -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);
}
}