Skip to content

Commit d8d0ddb

Browse files
authored
Git Blame - refactor implementation to use an additional dirty diff provider (microsoft#234420)
* Initial implementation using a quick diff provider * Add proposed API to hide a dirty diff decorator
1 parent 5498244 commit d8d0ddb

18 files changed

+192
-238
lines changed

extensions/git/src/blame.ts

+56-75
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, commands } from 'vscode';
6+
import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, QuickDiffProvider } from 'vscode';
77
import { Model } from './model';
88
import { dispose, fromNow, IDisposable, pathEquals } from './util';
99
import { Repository } from './repository';
1010
import { throttle } from './decorators';
1111
import { BlameInformation } from './git';
12+
import { toGitUri } from './uri';
1213

1314
function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean {
1415
return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive);
@@ -18,23 +19,6 @@ function lineRangeLength(startLineNumber: number, endLineNumberExclusive: number
1819
return endLineNumberExclusive - startLineNumber;
1920
}
2021

21-
function toTextEditorChange(originalStartLineNumber: number, originalEndLineNumberExclusive: number, modifiedStartLineNumber: number, modifiedEndLineNumberExclusive: number): TextEditorChange {
22-
let kind: TextEditorChangeKind;
23-
if (originalStartLineNumber === originalEndLineNumberExclusive) {
24-
kind = TextEditorChangeKind.Addition;
25-
} else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) {
26-
kind = TextEditorChangeKind.Deletion;
27-
} else {
28-
kind = TextEditorChangeKind.Modification;
29-
}
30-
31-
return {
32-
original: { startLineNumber: originalStartLineNumber, endLineNumberExclusive: originalEndLineNumberExclusive },
33-
modified: { startLineNumber: modifiedStartLineNumber, endLineNumberExclusive: modifiedEndLineNumberExclusive },
34-
kind
35-
};
36-
}
37-
3822
function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: readonly TextEditorChange[]): number {
3923
if (changes.length === 0) {
4024
return lineNumber;
@@ -79,14 +63,13 @@ interface LineBlameInformation {
7963
readonly blameInformation: BlameInformation | string;
8064
}
8165

82-
export class GitBlameController {
66+
export class GitBlameController implements QuickDiffProvider {
8367
private readonly _onDidChangeBlameInformation = new EventEmitter<TextEditor>();
8468
public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event;
8569

8670
readonly textEditorBlameInformation = new Map<TextEditor, readonly LineBlameInformation[]>();
8771

8872
private readonly _repositoryBlameInformation = new Map<Repository, RepositoryBlameInformation>();
89-
private readonly _stagedResourceDiffInformation = new Map<Repository, Map<Uri, TextEditorChange[]>>();
9073

9174
private _repositoryDisposables = new Map<Repository, IDisposable[]>();
9275
private _disposables: IDisposable[] = [];
@@ -104,6 +87,25 @@ export class GitBlameController {
10487
this._updateTextEditorBlameInformation(window.activeTextEditor);
10588
}
10689

90+
get visible(): boolean {
91+
return false;
92+
}
93+
94+
provideOriginalResource(uri: Uri): Uri | undefined {
95+
// Ignore resources outside a repository
96+
const repository = this._model.getRepository(uri);
97+
if (!repository) {
98+
return undefined;
99+
}
100+
101+
// Ignore resources that are not in the index group
102+
if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) {
103+
return undefined;
104+
}
105+
106+
return toGitUri(uri, 'HEAD', { replaceFileExtension: true });
107+
}
108+
107109
getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString {
108110
if (typeof blameInformation === 'string') {
109111
return new MarkdownString(blameInformation, true);
@@ -141,9 +143,7 @@ export class GitBlameController {
141143

142144
private _onDidOpenRepository(repository: Repository): void {
143145
const repositoryDisposables: IDisposable[] = [];
144-
145146
repository.onDidRunGitStatus(() => this._onDidRunGitStatus(repository), this, repositoryDisposables);
146-
repository.onDidChangeRepository(e => this._onDidChangeRepository(repository, e), this, this._disposables);
147147

148148
this._repositoryDisposables.set(repository, repositoryDisposables);
149149
}
@@ -174,14 +174,6 @@ export class GitBlameController {
174174
}
175175
}
176176

177-
private _onDidChangeRepository(repository: Repository, uri: Uri): void {
178-
if (!/\.git\/index$/.test(uri.fsPath)) {
179-
return;
180-
}
181-
182-
this._stagedResourceDiffInformation.delete(repository);
183-
}
184-
185177
private async _getBlameInformation(resource: Uri): Promise<BlameInformation[] | undefined> {
186178
const repository = this._model.getRepository(resource);
187179
if (!repository || !repository.HEAD?.commit) {
@@ -209,76 +201,65 @@ export class GitBlameController {
209201
return resourceBlameInformation;
210202
}
211203

212-
private async _getStagedResourceDiffInformation(uri: Uri): Promise<TextEditorChange[] | undefined> {
213-
const repository = this._model.getRepository(uri);
214-
if (!repository) {
215-
return undefined;
204+
@throttle
205+
private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise<void> {
206+
if (!textEditor?.diffInformation) {
207+
return;
216208
}
217209

218-
const [resource] = repository.indexGroup
219-
.resourceStates.filter(r => pathEquals(uri.fsPath, r.resourceUri.fsPath));
220-
221-
if (!resource || !resource.leftUri || !resource.rightUri) {
222-
return undefined;
223-
}
210+
// Working tree diff information
211+
const diffInformationWorkingTree = textEditor.diffInformation
212+
.filter(diff => diff.original?.scheme === 'git')
213+
.find(diff => {
214+
const query = JSON.parse(diff.original!.query) as { ref: string };
215+
return query.ref !== 'HEAD';
216+
});
224217

225-
const diffInformationMap = this._stagedResourceDiffInformation.get(repository) ?? new Map<Uri, TextEditorChange[]>();
226-
let changes = diffInformationMap.get(resource.resourceUri);
227-
if (changes) {
228-
return changes;
229-
}
218+
// Working tree + index diff information
219+
const diffInformationWorkingTreeAndIndex = textEditor.diffInformation
220+
.filter(diff => diff.original?.scheme === 'git')
221+
.find(diff => {
222+
const query = JSON.parse(diff.original!.query) as { ref: string };
223+
return query.ref === 'HEAD';
224+
});
230225

231-
// Get the diff information for the staged resource
232-
const diffInformation: [number, number, number, number][] = await commands.executeCommand('_workbench.internal.computeDiff', resource.leftUri, resource.rightUri);
233-
if (!diffInformation) {
234-
return undefined;
226+
// Working tree diff information is not present or it is stale
227+
if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) {
228+
return;
235229
}
236230

237-
changes = diffInformation.map(change => toTextEditorChange(change[0], change[1], change[2], change[3]));
238-
this._stagedResourceDiffInformation.set(repository, diffInformationMap.set(resource.resourceUri, changes));
239-
240-
return changes;
241-
}
242-
243-
@throttle
244-
private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise<void> {
245-
const diffInformation = textEditor?.diffInformation;
246-
if (!diffInformation || diffInformation.isStale) {
231+
// Working tree + index diff information is present and it is stale
232+
if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) {
247233
return;
248234
}
249235

236+
// For staged resources, we provide an additional "original resource" so that core can
237+
// compute the diff information that contains the changes from the working tree and the
238+
// index.
239+
const diffInformation = diffInformationWorkingTreeAndIndex ?? diffInformationWorkingTree;
240+
241+
// Git blame information
250242
const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri);
251243
if (!resourceBlameInformation) {
252244
return;
253245
}
254246

255-
// The diff information does not contain changes that have been staged. We need
256-
// to get the staged changes and if present, merge them with the diff information.
257-
const diffInformationStagedResources: TextEditorChange[] = await this._getStagedResourceDiffInformation(textEditor.document.uri) ?? [];
258-
259-
console.log('diffInformation', diffInformation.changes);
260-
console.log('diffInformationStagedResources', diffInformationStagedResources);
261-
262-
console.log('resourceBlameInformation', resourceBlameInformation);
263-
264247
const lineBlameInformation: LineBlameInformation[] = [];
265248
for (const lineNumber of textEditor.selections.map(s => s.active.line)) {
266-
// Check if the line is contained in the diff information
267-
if (lineRangesContainLine(diffInformation.changes, lineNumber + 1)) {
249+
// Check if the line is contained in the working tree diff information
250+
if (lineRangesContainLine(diffInformationWorkingTree.changes, lineNumber + 1)) {
268251
lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') });
269252
continue;
270253
}
271254

272-
// Check if the line is contained in the staged resources diff information
273-
if (lineRangesContainLine(diffInformationStagedResources, lineNumber + 1)) {
255+
// Check if the line is contained in the working tree + index diff information
256+
if (lineRangesContainLine(diffInformationWorkingTreeAndIndex?.changes ?? [], lineNumber + 1)) {
274257
lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') });
275258
continue;
276259
}
277260

278-
const diffInformationAll = [...diffInformation.changes, ...diffInformationStagedResources];
279-
280261
// Map the line number to the git blame ranges using the diff information
281-
const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, diffInformationAll);
262+
const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, diffInformation.changes);
282263
const blameInformation = resourceBlameInformation.find(blameInformation => {
283264
return blameInformation.ranges.find(range => {
284265
return lineNumberWithDiff >= range.startLineNumber && lineNumberWithDiff <= range.endLineNumber;

extensions/git/src/commands.ts

-12
Original file line numberDiff line numberDiff line change
@@ -1559,10 +1559,6 @@ export class CommandCenter {
15591559
async stageSelectedChanges(changes: LineChange[]): Promise<void> {
15601560
const textEditor = window.activeTextEditor;
15611561

1562-
this.logger.debug('[CommandCenter][stageSelectedChanges] changes:', changes);
1563-
this.logger.debug('[CommandCenter][stageSelectedChanges] diffInformation.changes:', textEditor?.diffInformation?.changes);
1564-
this.logger.debug('[CommandCenter][stageSelectedChanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale);
1565-
15661562
if (!textEditor) {
15671563
return;
15681564
}
@@ -1745,10 +1741,6 @@ export class CommandCenter {
17451741
async revertSelectedRanges(changes: LineChange[]): Promise<void> {
17461742
const textEditor = window.activeTextEditor;
17471743

1748-
this.logger.debug('[CommandCenter][revertSelectedRanges] changes:', changes);
1749-
this.logger.debug('[CommandCenter][revertSelectedRanges] diffInformation.changes:', textEditor?.diffInformation?.changes);
1750-
this.logger.debug('[CommandCenter][revertSelectedRanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale);
1751-
17521744
if (!textEditor) {
17531745
return;
17541746
}
@@ -1826,10 +1818,6 @@ export class CommandCenter {
18261818
async unstageSelectedRanges(changes: LineChange[]): Promise<void> {
18271819
const textEditor = window.activeTextEditor;
18281820

1829-
this.logger.debug('[CommandCenter][unstageSelectedRanges] changes:', changes);
1830-
this.logger.debug('[CommandCenter][unstageSelectedRanges] diffInformation.changes:', textEditor?.diffInformation?.changes);
1831-
this.logger.debug('[CommandCenter][unstageSelectedRanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale);
1832-
18331821
if (!textEditor) {
18341822
return;
18351823
}

extensions/git/src/main.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,17 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel,
113113
cc,
114114
new GitFileSystemProvider(model),
115115
new GitDecorations(model),
116-
new GitBlameController(model),
117116
new GitTimelineProvider(model, cc),
118117
new GitEditSessionIdentityProvider(model),
119118
new TerminalShellExecutionManager(model, logger)
120119
);
121120

121+
const blameController = new GitBlameController(model);
122+
disposables.push(blameController);
123+
124+
const quickDiffProvider = window.registerQuickDiffProvider({ scheme: 'file' }, blameController, 'Git local changes (working tree + index)');
125+
disposables.push(quickDiffProvider);
126+
122127
const postCommitCommandsProvider = new GitPostCommitCommandsProvider();
123128
model.registerPostCommitCommandsProvider(postCommitCommandsProvider);
124129

extensions/git/src/repository.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ export class Repository implements Disposable {
10211021
* Quick diff label
10221022
*/
10231023
get label(): string {
1024-
return l10n.t('Git local working changes');
1024+
return l10n.t('Git local changes (working tree)');
10251025
}
10261026

10271027
provideOriginalResource(uri: Uri): Uri | undefined {

extensions/git/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"src/**/*",
1212
"../../src/vscode-dts/vscode.d.ts",
1313
"../../src/vscode-dts/vscode.proposed.diffCommand.d.ts",
14+
"../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts",
1415
"../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts",
1516
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
1617
"../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts",

0 commit comments

Comments
 (0)