Skip to content

Commit b08044f

Browse files
author
Eric Amodio
committed
Adds blame "references" (wip)
1 parent 53bebc8 commit b08044f

File tree

12 files changed

+298
-43
lines changed

12 files changed

+298
-43
lines changed

blame.png

3.26 KB
Loading

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@
1919
],
2020
"main": "./out/src/extension",
2121
"contributes": {
22+
"commands": [{
23+
"command": "git.codelen.showBlameHistory",
24+
"title": "Show Blame History",
25+
"category": "Git"
26+
}]
2227
},
2328
"scripts": {
2429
"vscode:prepublish": "node ./node_modules/vscode/bin/compile",
2530
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
2631
"postinstall": "node ./node_modules/vscode/bin/install && tsc"
2732
},
2833
"dependencies": {
34+
"tmp": "^0.0.28"
2935
},
3036
"devDependencies": {
3137
"typescript": "^1.8.10",

src/codeLensProvider.ts

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
'use strict';
2-
import {CancellationToken, CodeLens, CodeLensProvider, commands, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
3-
import {IBlameLine, gitBlame} from './git';
2+
import {CancellationToken, CodeLens, CodeLensProvider, commands, Location, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
3+
import {Commands, VsCodeCommands} from './constants';
4+
import {IGitBlameLine, gitBlame} from './git';
5+
import {toGitBlameUri} from './contentProvider';
46
import * as moment from 'moment';
57

68
export class GitCodeLens extends CodeLens {
7-
constructor(public blame: Promise<IBlameLine[]>, public fileName: string, public blameRange: Range, range: Range) {
9+
constructor(private blame: Promise<IGitBlameLine[]>, public repoPath: string, public fileName: string, private blameRange: Range, range: Range) {
810
super(range);
11+
}
12+
13+
getBlameLines(): Promise<IGitBlameLine[]> {
14+
return this.blame.then(allLines => allLines.slice(this.blameRange.start.line, this.blameRange.end.line + 1));
15+
}
916

10-
this.blame = blame;
11-
this.fileName = fileName;
12-
this.blameRange = blameRange;
17+
static toUri(lens: GitCodeLens, line: IGitBlameLine, lines: IGitBlameLine[]): Uri {
18+
return toGitBlameUri(Object.assign({ repoPath: lens.repoPath, range: lens.blameRange, lines: lines }, line));
1319
}
1420
}
1521

@@ -20,14 +26,14 @@ export default class GitCodeLensProvider implements CodeLensProvider {
2026
// TODO: Should I wait here?
2127
let blame = gitBlame(document.fileName);
2228

23-
return (commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri) as Promise<SymbolInformation[]>).then(symbols => {
29+
return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<SymbolInformation[]>).then(symbols => {
2430
let lenses: CodeLens[] = [];
2531
symbols.forEach(sym => this._provideCodeLens(document, sym, blame, lenses));
2632
return lenses;
2733
});
2834
}
2935

30-
private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, blame: Promise<IBlameLine[]>, lenses: CodeLens[]): void {
36+
private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, blame: Promise<IGitBlameLine[]>, lenses: CodeLens[]): void {
3137
switch (symbol.kind) {
3238
case SymbolKind.Module:
3339
case SymbolKind.Class:
@@ -43,30 +49,46 @@ export default class GitCodeLensProvider implements CodeLensProvider {
4349
}
4450

4551
var line = document.lineAt(symbol.location.range.start);
46-
// if (line.text.includes(symbol.name)) {
47-
// }
48-
49-
let lens = new GitCodeLens(blame, document.fileName, symbol.location.range, line.range);
52+
let lens = new GitCodeLens(blame, this.repoPath, document.fileName, symbol.location.range, line.range);
5053
lenses.push(lens);
5154
}
5255

53-
resolveCodeLens(codeLens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
54-
if (codeLens instanceof GitCodeLens) {
55-
return codeLens.blame.then(allLines => {
56-
let lines = allLines.slice(codeLens.blameRange.start.line, codeLens.blameRange.end.line + 1);
57-
let line = lines[0];
56+
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
57+
if (lens instanceof GitCodeLens) {
58+
return lens.getBlameLines().then(lines => {
59+
let recentLine = lines[0];
60+
61+
let locations: Location[] = [];
5862
if (lines.length > 1) {
59-
let sorted = lines.sort((a, b) => b.date.getTime() - a.date.getTime());
60-
line = sorted[0];
63+
let sorted = lines.sort((a, b) => a.date.getTime() - b.date.getTime());
64+
recentLine = sorted[sorted.length - 1];
65+
66+
console.log(lens.fileName, 'Blame lines:', sorted);
67+
68+
let map: Map<string, IGitBlameLine[]> = new Map();
69+
sorted.forEach(l => {
70+
let item = map.get(l.sha);
71+
if (item) {
72+
item.push(l);
73+
} else {
74+
map.set(l.sha, [l]);
75+
}
76+
});
77+
78+
locations = Array.from(map.values()).map(l => new Location(GitCodeLens.toUri(lens, l[0], l), lens.range.start))
79+
} else {
80+
locations = [new Location(GitCodeLens.toUri(lens, recentLine, lines), lens.range.start)];
6181
}
6282

63-
codeLens.command = {
64-
title: `${line.author}, ${moment(line.date).fromNow()}`,
65-
command: 'git.viewFileHistory',
66-
arguments: [Uri.file(codeLens.fileName)]
83+
lens.command = {
84+
title: `${recentLine.author}, ${moment(recentLine.date).fromNow()}`,
85+
command: Commands.ShowBlameHistory,
86+
arguments: [Uri.file(lens.fileName), lens.range.start, locations]
87+
// command: 'git.viewFileHistory',
88+
// arguments: [Uri.file(codeLens.fileName)]
6789
};
68-
return codeLens;
69-
});//.catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing
90+
return lens;
91+
}).catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing
7092
}
7193
}
7294
}

src/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export type Commands = 'git.action.showBlameHistory';
2+
export const Commands = {
3+
ShowBlameHistory: 'git.action.showBlameHistory' as Commands
4+
}
5+
6+
export type DocumentSchemes = 'gitblame';
7+
export const DocumentSchemes = {
8+
GitBlame: 'gitblame' as DocumentSchemes
9+
}
10+
11+
export type VsCodeCommands = 'vscode.executeDocumentSymbolProvider' | 'editor.action.showReferences';
12+
export const VsCodeCommands = {
13+
ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as VsCodeCommands,
14+
ShowReferences: 'editor.action.showReferences' as VsCodeCommands
15+
}

src/contentProvider.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
import {Disposable, EventEmitter, ExtensionContext, Location, OverviewRulerLane, Range, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode';
3+
import {DocumentSchemes} from './constants';
4+
import {gitGetVersionFile, gitGetVersionText, IGitBlameLine} from './git';
5+
import {basename, dirname, extname} from 'path';
6+
import * as moment from 'moment';
7+
8+
export default class GitBlameContentProvider implements TextDocumentContentProvider {
9+
static scheme = DocumentSchemes.GitBlame;
10+
11+
private _blameDecoration: TextEditorDecorationType;
12+
private _onDidChange = new EventEmitter<Uri>();
13+
14+
constructor(context: ExtensionContext) {
15+
let image = context.asAbsolutePath('blame.png');
16+
this._blameDecoration = window.createTextEditorDecorationType({
17+
backgroundColor: 'rgba(21, 251, 126, 0.7)',
18+
gutterIconPath: image,
19+
gutterIconSize: 'auto'
20+
});
21+
}
22+
23+
dispose() {
24+
this._onDidChange.dispose();
25+
}
26+
27+
get onDidChange() {
28+
return this._onDidChange.event;
29+
}
30+
31+
public update(uri: Uri) {
32+
this._onDidChange.fire(uri);
33+
}
34+
35+
provideTextDocumentContent(uri: Uri): string | Thenable<string> {
36+
const data = fromGitBlameUri(uri);
37+
38+
console.log('provideTextDocumentContent', uri, data);
39+
return gitGetVersionText(data.repoPath, data.sha, data.file).then(text => {
40+
this.update(uri);
41+
42+
setTimeout(() => {
43+
let uriString = uri.toString();
44+
let editor = window.visibleTextEditors.find((e: any) => (e._documentData && e._documentData._uri && e._documentData._uri.toString()) === uriString);
45+
if (editor) {
46+
editor.setDecorations(this._blameDecoration, data.lines.map(l => new Range(l.line, 0, l.line, 1)));
47+
}
48+
}, 1500);
49+
50+
// let foo = text.split('\n');
51+
// return foo.slice(data.range.start.line, data.range.end.line).join('\n')
52+
return text;
53+
});
54+
55+
// return gitGetVersionFile(data.repoPath, data.sha, data.file).then(dst => {
56+
// let uri = Uri.parse(`file:${dst}`)
57+
// return workspace.openTextDocument(uri).then(doc => {
58+
// this.update(uri);
59+
// return doc.getText();
60+
// });
61+
// });
62+
}
63+
}
64+
65+
export interface IGitBlameUriData extends IGitBlameLine {
66+
repoPath: string,
67+
range: Range,
68+
lines: IGitBlameLine[]
69+
}
70+
71+
export function toGitBlameUri(data: IGitBlameUriData) {
72+
let ext = extname(data.file);
73+
let path = `${dirname(data.file)}/${data.sha}: ${basename(data.file, ext)}${ext}`;
74+
return Uri.parse(`${DocumentSchemes.GitBlame}:${path}?${JSON.stringify(data)}`);
75+
}
76+
77+
export function fromGitBlameUri(uri: Uri): IGitBlameUriData {
78+
let data = JSON.parse(uri.query);
79+
data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character);
80+
return data;
81+
}

src/definitionProvider.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// 'use strict';
2+
// import {CancellationToken, CodeLens, commands, DefinitionProvider, Position, Location, TextDocument, Uri} from 'vscode';
3+
// import {GitCodeLens} from './codeLensProvider';
4+
5+
// export default class GitDefinitionProvider implements DefinitionProvider {
6+
// public provideDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise<Location> {
7+
// return (commands.executeCommand('vscode.executeCodeLensProvider', document.uri) as Promise<CodeLens[]>).then(lenses => {
8+
// let matches: CodeLens[] = [];
9+
// lenses.forEach(lens => {
10+
// if (lens instanceof GitCodeLens && lens.blameRange.contains(position)) {
11+
// matches.push(lens);
12+
// }
13+
// });
14+
15+
// if (matches.length) {
16+
// return new Location(Uri.parse(), position);
17+
// }
18+
// return null;
19+
// });
20+
// }
21+
// }

src/extension.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
'use strict';
2-
import {DocumentSelector, ExtensionContext, languages, workspace} from 'vscode';
2+
import {commands, DocumentSelector, ExtensionContext, languages, workspace} from 'vscode';
33
import GitCodeLensProvider from './codeLensProvider';
4-
import {gitRepoPath} from './git'
4+
import GitContentProvider from './contentProvider';
5+
import {gitRepoPath} from './git';
6+
import {Commands, VsCodeCommands} from './constants';
57

68
// this method is called when your extension is activated
79
export function activate(context: ExtensionContext) {
@@ -11,6 +13,12 @@ export function activate(context: ExtensionContext) {
1113
}
1214

1315
gitRepoPath(workspace.rootPath).then(repoPath => {
16+
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context)));
17+
18+
context.subscriptions.push(commands.registerCommand(Commands.ShowBlameHistory, (...args) => {
19+
return commands.executeCommand(VsCodeCommands.ShowReferences, ...args);
20+
}));
21+
1422
let selector: DocumentSelector = { scheme: 'file' };
1523
context.subscriptions.push(languages.registerCodeLensProvider(selector, new GitCodeLensProvider(repoPath)));
1624
}).catch(reason => console.warn(reason));

src/git.ts

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
'use strict';
22
import {spawn} from 'child_process';
3-
import {dirname} from 'path';
3+
import {basename, dirname, extname} from 'path';
4+
import * as fs from 'fs';
5+
import * as tmp from 'tmp';
46

5-
export declare interface IBlameLine {
6-
line: number;
7+
export declare interface IGitBlameLine {
8+
sha: string;
9+
file: string;
10+
originalLine: number;
711
author: string;
812
date: Date;
9-
sha: string;
10-
//code: string;
13+
line: number;
14+
code: string;
1115
}
1216

1317
export function gitRepoPath(cwd) {
@@ -22,33 +26,72 @@ export function gitRepoPath(cwd) {
2226
});
2327
}
2428

25-
const blameMatcher = /^(.*)\t\((.*)\t(.*)\t(.*?)\)(.*)$/gm;
29+
//const blameMatcher = /^(.*)\t\((.*)\t(.*)\t(.*?)\)(.*)$/gm;
30+
const blameMatcher = /^([0-9a-fA-F]{8})\s([\S]*)\s([0-9\S]+)\s\((.*?)\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[-|+][0-9]{4})\s([0-9]+)\)(.*)$/gm;
2631

27-
export function gitBlame(fileName: string) {
32+
export function gitBlame(fileName: string): Promise<IGitBlameLine[]> {
2833
const mapper = (input, output) => {
34+
let blame = input.toString();
35+
console.log(fileName, 'Blame:', blame);
36+
2937
let m: Array<string>;
30-
while ((m = blameMatcher.exec(input.toString())) != null) {
38+
while ((m = blameMatcher.exec(blame)) != null) {
3139
output.push({
32-
line: parseInt(m[4], 10),
33-
author: m[2],
34-
date: new Date(m[3]),
35-
sha: m[1]
36-
//code: m[5]
40+
sha: m[1],
41+
file: m[2].trim(),
42+
originalLine: parseInt(m[3], 10),
43+
author: m[4].trim(),
44+
date: new Date(m[5]),
45+
line: parseInt(m[6], 10),
46+
code: m[7]
3747
});
3848
}
3949
};
4050

41-
return gitCommand(dirname(fileName), mapper, 'blame', '-c', '-M', '-w', '--', fileName) as Promise<IBlameLine[]>;
51+
return gitCommand(dirname(fileName), mapper, 'blame', '-fnw', '--', fileName);
52+
}
53+
54+
export function gitGetVersionFile(repoPath: string, sha: string, source: string): Promise<any> {
55+
const mapper = (input, output) => output.push(input);
56+
57+
return new Promise<string>((resolve, reject) => {
58+
(gitCommand(repoPath, mapper, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise<Array<Buffer>>).then(o => {
59+
let ext = extname(source);
60+
tmp.file({ prefix: `${basename(source, ext)}-${sha}_`, postfix: ext }, (err, destination, fd, cleanupCallback) => {
61+
if (err) {
62+
reject(err);
63+
return;
64+
}
65+
66+
console.log("File: ", destination);
67+
console.log("Filedescriptor: ", fd);
68+
69+
fs.appendFile(destination, o.join(), err => {
70+
if (err) {
71+
reject(err);
72+
return;
73+
}
74+
resolve(destination);
75+
});
76+
});
77+
});
78+
});
79+
}
80+
81+
export function gitGetVersionText(repoPath: string, sha: string, source: string): Promise<string> {
82+
const mapper = (input, output) => output.push(input.toString());
83+
84+
return new Promise<string>((resolve, reject) => (gitCommand(repoPath, mapper, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise<Array<string>>).then(o => resolve(o.join())));
4285
}
4386

44-
function gitCommand(cwd: string, map: (input: Buffer, output: Array<any>) => void, ...args): Promise<any> {
87+
function gitCommand(cwd: string, mapper: (input: Buffer, output: Array<any>) => void, ...args): Promise<any> {
4588
return new Promise<any>((resolve, reject) => {
4689
let spawn = require('child_process').spawn;
4790
let process = spawn('git', args, { cwd: cwd });
4891

4992
let output: Array<any> = [];
5093
process.stdout.on('data', data => {
51-
map(data, output);
94+
mapper(data, output);
5295
});
5396

5497
let errors: Array<string> = [];

typings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"globalDependencies": {
3+
"tmp": "registry:dt/tmp#0.0.0+20160514170650"
4+
}
5+
}

0 commit comments

Comments
 (0)