Skip to content

Commit 70cc92d

Browse files
author
Eric Amodio
committed
Adds CodeLens for Diff'ing in blame
Other fixes and refactoring
1 parent 0e064f1 commit 70cc92d

File tree

9 files changed

+162
-29
lines changed

9 files changed

+162
-29
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitlens",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"author": "Eric Amodio",
55
"publisher": "eamodio",
66
"engines": {

src/commands.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {commands, Disposable, Position, Range, Uri, window} from 'vscode';
33
import {Commands, VsCodeCommands} from './constants';
44
import GitProvider from './gitProvider';
5+
import {basename} from 'path';
56

67
abstract class Command extends Disposable {
78
private _subscriptions: Disposable;
@@ -41,4 +42,33 @@ export class BlameCommand extends Command {
4142
return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations);
4243
});
4344
}
45+
}
46+
47+
export class DiffWithPreviousCommand extends Command {
48+
constructor(private git: GitProvider) {
49+
super(Commands.DiffWithPrevious);
50+
}
51+
52+
execute(uri?: Uri, sha?: string, compareWithSha?: string) {
53+
// TODO: Execute these in parallel rather than series
54+
return this.git.getVersionedFile(uri.path, sha).then(source => {
55+
this.git.getVersionedFile(uri.path, compareWithSha).then(compare => {
56+
const fileName = basename(uri.path);
57+
return commands.executeCommand(VsCodeCommands.Diff, Uri.file(source), Uri.file(compare), `${fileName} (${sha}) ↔ ${fileName} (${compareWithSha})`);
58+
})
59+
});
60+
}
61+
}
62+
63+
export class DiffWithWorkingCommand extends Command {
64+
constructor(private git: GitProvider) {
65+
super(Commands.DiffWithWorking);
66+
}
67+
68+
execute(uri?: Uri, sha?: string) {
69+
return this.git.getVersionedFile(uri.path, sha).then(compare => {
70+
const fileName = basename(uri.path);
71+
return commands.executeCommand(VsCodeCommands.Diff, uri, Uri.file(compare), `${fileName} (index) ↔ ${fileName} (${sha})`);
72+
});
73+
}
4474
}

src/constants.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ export const WorkspaceState = {
77

88
export const RepoPath: string = 'repoPath';
99

10-
export type Commands = 'git.action.showBlameHistory';
10+
export type Commands = 'git.action.diffWithPrevious' | 'git.action.diffWithWorking' | 'git.action.showBlameHistory';
1111
export const Commands = {
12+
DiffWithPrevious: 'git.action.diffWithPrevious' as Commands,
13+
DiffWithWorking: 'git.action.diffWithWorking' as Commands,
1214
ShowBlameHistory: 'git.action.showBlameHistory' as Commands
1315
}
1416

15-
export type DocumentSchemes = 'gitblame';
17+
export type DocumentSchemes = 'file' | 'gitblame';
1618
export const DocumentSchemes = {
19+
File: 'file' as DocumentSchemes,
1720
GitBlame: 'gitblame' as DocumentSchemes
1821
}
1922

20-
export type VsCodeCommands = 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider' | 'editor.action.showReferences';
23+
export type VsCodeCommands = 'vscode.diff' | 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider' | 'editor.action.showReferences';
2124
export const VsCodeCommands = {
25+
Diff: 'vscode.diff' as VsCodeCommands,
2226
ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as VsCodeCommands,
2327
ExecuteCodeLensProvider: 'vscode.executeCodeLensProvider' as VsCodeCommands,
2428
ShowReferences: 'editor.action.showReferences' as VsCodeCommands

src/extension.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use strict';
22
import {CodeLens, DocumentSelector, ExtensionContext, languages, workspace} from 'vscode';
3-
import GitCodeLensProvider, {GitBlameCodeLens} from './codeLensProvider';
4-
import GitContentProvider from './contentProvider';
3+
import GitCodeLensProvider from './gitCodeLensProvider';
4+
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
5+
import GitBlameContentProvider from './gitBlameContentProvider';
56
import GitProvider from './gitProvider';
6-
import {BlameCommand} from './commands';
7+
import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
78
import {WorkspaceState} from './constants';
89

910
// this method is called when your extension is activated
@@ -23,12 +24,12 @@ export function activate(context: ExtensionContext) {
2324
git.getRepoPath(workspace.rootPath).then(repoPath => {
2425
context.workspaceState.update(WorkspaceState.RepoPath, repoPath);
2526

26-
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git)));
27-
27+
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git)));
28+
context.subscriptions.push(languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(context, git)));
29+
context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git)));
2830
context.subscriptions.push(new BlameCommand(git));
29-
30-
const selector: DocumentSelector = { scheme: 'file' };
31-
context.subscriptions.push(languages.registerCodeLensProvider(selector, new GitCodeLensProvider(context, git)));
31+
context.subscriptions.push(new DiffWithPreviousCommand(git));
32+
context.subscriptions.push(new DiffWithWorkingCommand(git));
3233
}).catch(reason => console.warn(reason));
3334
}
3435

src/git.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ export default class Git {
1818
return gitCommand(cwd, 'rev-parse', '--show-toplevel').then(data => data.replace(/\r?\n|\r/g, ''));
1919
}
2020

21-
static blame(fileName: string, repoPath: string) {
21+
static blame(fileName: string, repoPath: string, sha?: string) {
2222
fileName = Git.normalizePath(fileName, repoPath);
2323

24+
if (sha) {
25+
console.log('git', 'blame', '-fnw', '--root', `${sha}^`, '--', fileName);
26+
return gitCommand(repoPath, 'blame', '-fnw', '--root', `${sha}^`, '--', fileName);
27+
}
28+
2429
console.log('git', 'blame', '-fnw', '--root', '--', fileName);
2530
return gitCommand(repoPath, 'blame', '-fnw', '--root', '--', fileName);
2631
// .then(s => { console.log(s); return s; })

src/gitBlameCodeLensProvider.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
3+
import {Commands, DocumentSchemes, VsCodeCommands, WorkspaceState} from './constants';
4+
import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider';
5+
import {join} from 'path';
6+
import * as moment from 'moment';
7+
8+
export class GitDiffWithWorkingTreeCodeLens extends CodeLens {
9+
constructor(private git: GitProvider, public fileName: string, public sha: string, range: Range) {
10+
super(range);
11+
}
12+
}
13+
14+
export class GitDiffWithPreviousCodeLens extends CodeLens {
15+
constructor(private git: GitProvider, public fileName: string, public sha: string, public compareWithSha: string, range: Range) {
16+
super(range);
17+
}
18+
}
19+
20+
export default class GitBlameCodeLensProvider implements CodeLensProvider {
21+
static selector: DocumentSelector = { scheme: DocumentSchemes.GitBlame };
22+
23+
constructor(context: ExtensionContext, private git: GitProvider) { }
24+
25+
provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable<CodeLens[]> {
26+
const data = this.git.fromBlameUri(document.uri);
27+
const fileName = data.fileName;
28+
29+
return this.git.getBlameForFile(fileName).then(blame => {
30+
const commits = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime());
31+
let index = commits.findIndex(c => c.sha === data.sha) + 1;
32+
33+
let previousCommit: IGitBlameCommit;
34+
if (index < commits.length) {
35+
previousCommit = commits[index];
36+
}
37+
38+
const lenses: CodeLens[] = [];
39+
40+
// Add codelens to each "group" of blame lines
41+
const lines = blame.lines.filter(l => l.sha === data.sha);
42+
let lastLine = lines[0].originalLine;
43+
lines.forEach(l => {
44+
if (l.originalLine !== lastLine + 1) {
45+
lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, data.sha, new Range(l.originalLine, 0, l.originalLine, 1)));
46+
if (previousCommit) {
47+
lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, data.sha, previousCommit.sha, new Range(l.originalLine, 1, l.originalLine, 2)));
48+
}
49+
}
50+
lastLine = l.originalLine;
51+
});
52+
53+
// Check if we have a lens for the whole document -- if not add one
54+
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
55+
lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, data.sha, new Range(0, 0, 0, 1)));
56+
if (previousCommit) {
57+
lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, data.sha, previousCommit.sha, new Range(0, 1, 0, 2)));
58+
}
59+
}
60+
61+
return lenses;
62+
});
63+
}
64+
65+
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
66+
if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token);
67+
if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token);
68+
}
69+
70+
_resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable<CodeLens> {
71+
lens.command = {
72+
title: `Compare with Working Tree`,
73+
command: Commands.DiffWithWorking,
74+
arguments: [Uri.file(join(this.git.repoPath, lens.fileName)), lens.sha]
75+
};
76+
return Promise.resolve(lens);
77+
}
78+
79+
_resolveGitDiffWithPreviousCodeLens(lens: GitDiffWithPreviousCodeLens, token: CancellationToken): Thenable<CodeLens> {
80+
lens.command = {
81+
title: `Compare with Previous (${lens.compareWithSha})`,
82+
command: Commands.DiffWithPrevious,
83+
arguments: [Uri.file(join(this.git.repoPath, lens.fileName)), lens.sha, lens.compareWithSha]
84+
};
85+
return Promise.resolve(lens);
86+
}
87+
}
File renamed without changes.

src/codeLensProvider.ts renamed to src/gitCodeLensProvider.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use strict';
2-
import {CancellationToken, CodeLens, CodeLensProvider, commands, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
3-
import {Commands, VsCodeCommands, WorkspaceState} from './constants';
2+
import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
3+
import {Commands, DocumentSchemes, VsCodeCommands, WorkspaceState} from './constants';
44
import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider';
55
import * as moment from 'moment';
66

7-
export class GitBlameCodeLens extends CodeLens {
7+
export class GitCodeLens extends CodeLens {
88
constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) {
99
super(range);
1010
}
@@ -21,26 +21,30 @@ export class GitHistoryCodeLens extends CodeLens {
2121
}
2222

2323
export default class GitCodeLensProvider implements CodeLensProvider {
24+
static selector: DocumentSelector = { scheme: DocumentSchemes.File };
25+
2426
constructor(context: ExtensionContext, private git: GitProvider) { }
2527

2628
provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable<CodeLens[]> {
27-
this.git.getBlameForFile(document.fileName);
29+
const fileName = document.fileName;
30+
31+
this.git.getBlameForFile(fileName);
2832

2933
return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<SymbolInformation[]>).then(symbols => {
30-
let lenses: CodeLens[] = [];
31-
symbols.forEach(sym => this._provideCodeLens(document, sym, lenses));
34+
const lenses: CodeLens[] = [];
35+
symbols.forEach(sym => this._provideCodeLens(fileName, document, sym, lenses));
3236

3337
// Check if we have a lens for the whole document -- if not add one
3438
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
35-
const docRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
36-
lenses.push(new GitBlameCodeLens(this.git, document.fileName, docRange, new Range(0, 0, 0, docRange.start.character)));
37-
lenses.push(new GitHistoryCodeLens(this.git.repoPath, document.fileName, new Range(0, 1, 0, docRange.start.character)));
39+
const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
40+
lenses.push(new GitCodeLens(this.git, fileName, blameRange, new Range(0, 0, 0, blameRange.start.character)));
41+
lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, new Range(0, 1, 0, blameRange.start.character)));
3842
}
3943
return lenses;
4044
});
4145
}
4246

43-
private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void {
47+
private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void {
4448
switch (symbol.kind) {
4549
case SymbolKind.Package:
4650
case SymbolKind.Module:
@@ -66,16 +70,16 @@ export default class GitCodeLensProvider implements CodeLensProvider {
6670
startChar += Math.floor(symbol.name.length / 2);
6771
}
6872

69-
lenses.push(new GitBlameCodeLens(this.git, document.fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar))));
70-
lenses.push(new GitHistoryCodeLens(this.git.repoPath, document.fileName, line.range.with(new Position(line.range.start.line, startChar + 1))));
73+
lenses.push(new GitCodeLens(this.git, fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar))));
74+
lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, line.range.with(new Position(line.range.start.line, startChar + 1))));
7175
}
7276

7377
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
74-
if (lens instanceof GitBlameCodeLens) return this._resolveGitBlameCodeLens(lens, token);
78+
if (lens instanceof GitCodeLens) return this._resolveGitBlameCodeLens(lens, token);
7579
if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token);
7680
}
7781

78-
_resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable<CodeLens> {
82+
_resolveGitBlameCodeLens(lens: GitCodeLens, token: CancellationToken): Thenable<CodeLens> {
7983
return new Promise<CodeLens>((resolve, reject) => {
8084
lens.getBlame().then(blame => {
8185
if (!blame.lines.length) {
@@ -104,4 +108,4 @@ export default class GitCodeLensProvider implements CodeLensProvider {
104108
};
105109
return Promise.resolve(lens);
106110
}
107-
}
111+
}

src/gitProvider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export default class GitProvider extends Disposable {
151151
return Uri.parse(`${DocumentSchemes.GitBlame}:${pad(index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MM a')} - ${path}?${JSON.stringify(data)}`);
152152
}
153153

154-
fromBlameUri(uri: Uri) {
154+
fromBlameUri(uri: Uri): IGitBlameUriData {
155155
const data = JSON.parse(uri.query);
156156
data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character);
157157
return data;
@@ -169,13 +169,15 @@ export interface IGitBlameCommit {
169169
author: string;
170170
date: Date;
171171
}
172+
172173
export interface IGitBlameLine {
173174
sha: string;
174175
line: number;
175176
originalLine: number;
176177
originalFileName?: string;
177178
code?: string;
178179
}
180+
179181
export interface IGitBlameUriData {
180182
fileName: string,
181183
originalFileName?: string;

0 commit comments

Comments
 (0)