From 03d4cc85972c5eac61bd858a18c880714aebd69a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 14 Aug 2016 02:03:36 -0400 Subject: [PATCH] Richer blame info & highlight (wip) --- blame.png | Bin 3340 -> 24733 bytes src/codeLensProvider.ts | 70 +++++++++++++++++++++++++++++++---------- src/contentProvider.ts | 81 ++++++++++++++++++++++++++++++++++++++---------- src/extension.ts | 2 +- src/git.ts | 67 ++++++++++++++++++++++----------------- 5 files changed, 158 insertions(+), 62 deletions(-) diff --git a/blame.png b/blame.png index 2152961cf1942aa7db2170539bd42a7f6659d15e..22f8bcd53767b413a3498fd74adf6085a1e3383b 100644 GIT binary patch literal 24733 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 diff --git a/src/codeLensProvider.ts b/src/codeLensProvider.ts index e385f4f..96738ec 100644 --- a/src/codeLensProvider.ts +++ b/src/codeLensProvider.ts @@ -1,11 +1,11 @@ 'use strict'; -import {CancellationToken, CodeLens, CodeLensProvider, commands, Location, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; import {Commands, VsCodeCommands} from './constants'; import {IGitBlameLine, gitBlame} from './git'; import {toGitBlameUri} from './contentProvider'; import * as moment from 'moment'; -export class GitCodeLens extends CodeLens { +export class GitBlameCodeLens extends CodeLens { constructor(private blame: Promise, public repoPath: string, public fileName: string, private blameRange: Range, range: Range) { super(range); } @@ -14,11 +14,21 @@ export class GitCodeLens extends CodeLens { return this.blame.then(allLines => allLines.slice(this.blameRange.start.line, this.blameRange.end.line + 1)); } - static toUri(lens: GitCodeLens, line: IGitBlameLine, lines: IGitBlameLine[]): Uri { - return toGitBlameUri(Object.assign({ repoPath: lens.repoPath, range: lens.blameRange, lines: lines }, line)); + static toUri(lens: GitBlameCodeLens, index: number, line: IGitBlameLine, lines: IGitBlameLine[]): Uri { + return toGitBlameUri(Object.assign({ repoPath: lens.repoPath, index: index, range: lens.blameRange, lines: lines }, line)); } } +export class GitHistoryCodeLens extends CodeLens { + constructor(public repoPath: string, public fileName: string, range: Range) { + super(range); + } + + // static toUri(lens: GitHistoryCodeLens, index: number): Uri { + // return toGitBlameUri(Object.assign({ repoPath: lens.repoPath, index: index, range: lens.blameRange, lines: lines }, line)); + // } +} + export default class GitCodeLensProvider implements CodeLensProvider { constructor(public repoPath: string) { } @@ -29,39 +39,52 @@ export default class GitCodeLensProvider implements CodeLensProvider { return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise).then(symbols => { let lenses: CodeLens[] = []; symbols.forEach(sym => this._provideCodeLens(document, sym, blame, lenses)); + + // Check if we have a lens for the whole document -- if not add one + if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { + const docRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); + lenses.push(new GitBlameCodeLens(blame, this.repoPath, document.fileName, docRange, new Range(0, 0, 0, docRange.start.character))); + } return lenses; }); } private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, blame: Promise, lenses: CodeLens[]): void { switch (symbol.kind) { + case SymbolKind.Package: case SymbolKind.Module: case SymbolKind.Class: case SymbolKind.Interface: - case SymbolKind.Method: - case SymbolKind.Function: case SymbolKind.Constructor: - case SymbolKind.Field: + case SymbolKind.Method: case SymbolKind.Property: + case SymbolKind.Field: + case SymbolKind.Function: + case SymbolKind.Enum: break; default: return; } var line = document.lineAt(symbol.location.range.start); - let lens = new GitCodeLens(blame, this.repoPath, document.fileName, symbol.location.range, line.range); - lenses.push(lens); + lenses.push(new GitBlameCodeLens(blame, this.repoPath, document.fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, line.firstNonWhitespaceCharacterIndex)))); + lenses.push(new GitHistoryCodeLens(this.repoPath, document.fileName, line.range.with(new Position(line.range.start.line, line.firstNonWhitespaceCharacterIndex + 1)))); } resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { - if (lens instanceof GitCodeLens) { + if (lens instanceof GitBlameCodeLens) { return lens.getBlameLines().then(lines => { + if (!lines.length) { + console.error('No blame lines found', lens); + throw new Error('No blame lines found'); + } + let recentLine = lines[0]; let locations: Location[] = []; if (lines.length > 1) { - let sorted = lines.sort((a, b) => a.date.getTime() - b.date.getTime()); - recentLine = sorted[sorted.length - 1]; + let sorted = lines.sort((a, b) => b.date.getTime() - a.date.getTime()); + recentLine = sorted[0]; console.log(lens.fileName, 'Blame lines:', sorted); @@ -75,20 +98,35 @@ export default class GitCodeLensProvider implements CodeLensProvider { } }); - locations = Array.from(map.values()).map(l => new Location(GitCodeLens.toUri(lens, l[0], l), lens.range.start)) + Array.from(map.values()).forEach((lines, i) => { + const uri = GitBlameCodeLens.toUri(lens, i + 1, lines[0], lines); + lines.forEach(l => { + locations.push(new Location(uri, new Position(l.originalLine, 0))); + }); + }); + + //locations = Array.from(map.values()).map((l, i) => new Location(GitBlameCodeLens.toUri(lens, i, l[0], l), new Position(l[0].originalLine, 0)));//lens.range.start)) } else { - locations = [new Location(GitCodeLens.toUri(lens, recentLine, lines), lens.range.start)]; + locations = [new Location(GitBlameCodeLens.toUri(lens, 1, recentLine, lines), lens.range.start)]; } lens.command = { title: `${recentLine.author}, ${moment(recentLine.date).fromNow()}`, command: Commands.ShowBlameHistory, arguments: [Uri.file(lens.fileName), lens.range.start, locations] - // command: 'git.viewFileHistory', - // arguments: [Uri.file(codeLens.fileName)] }; return lens; }).catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing } + + // TODO: Play with this more -- get this to open the correct diff to the right place + if (lens instanceof GitHistoryCodeLens) { + lens.command = { + title: `View Diff`, + command: 'git.viewFileHistory', // viewLineHistory + arguments: [Uri.file(lens.fileName)] + }; + return Promise.resolve(lens); + } } } diff --git a/src/contentProvider.ts b/src/contentProvider.ts index 269a2d1..0847eb1 100644 --- a/src/contentProvider.ts +++ b/src/contentProvider.ts @@ -1,8 +1,8 @@ 'use strict'; -import {Disposable, EventEmitter, ExtensionContext, Location, OverviewRulerLane, Range, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode'; +import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode'; import {DocumentSchemes} from './constants'; import {gitGetVersionFile, gitGetVersionText, IGitBlameLine} from './git'; -import {basename, dirname, extname} from 'path'; +import {basename, dirname, extname, join} from 'path'; import * as moment from 'moment'; export default class GitBlameContentProvider implements TextDocumentContentProvider { @@ -10,18 +10,37 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi private _blameDecoration: TextEditorDecorationType; private _onDidChange = new EventEmitter(); + private _subscriptions: Disposable; + // private _dataMap: Map; constructor(context: ExtensionContext) { - let image = context.asAbsolutePath('blame.png'); + // TODO: Light & Dark this._blameDecoration = window.createTextEditorDecorationType({ - backgroundColor: 'rgba(21, 251, 126, 0.7)', - gutterIconPath: image, - gutterIconSize: 'auto' + backgroundColor: 'rgba(254, 220, 95, 0.15)', + gutterIconPath: context.asAbsolutePath('blame.png'), + overviewRulerColor: 'rgba(254, 220, 95, 0.60)', + overviewRulerLane: OverviewRulerLane.Right, + isWholeLine: true }); + + // this._dataMap = new Map(); + // this._subscriptions = Disposable.from( + // workspace.onDidOpenTextDocument(d => { + // let data = this._dataMap.get(d.uri.toString()); + // if (!data) return; + + // // TODO: This only works on the first load -- not after since it is cached + // this._tryAddBlameDecorations(d.uri, data); + // }), + // workspace.onDidCloseTextDocument(d => { + // this._dataMap.delete(d.uri.toString()); + // }) + // ); } dispose() { this._onDidChange.dispose(); + this._subscriptions && this._subscriptions.dispose(); } get onDidChange() { @@ -34,21 +53,20 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi provideTextDocumentContent(uri: Uri): string | Thenable { const data = fromGitBlameUri(uri); + // this._dataMap.set(uri.toString(), data); + + //const editor = this._findEditor(Uri.file(join(data.repoPath, data.file))); - console.log('provideTextDocumentContent', uri, data); + //console.log('provideTextDocumentContent', uri, data); return gitGetVersionText(data.repoPath, data.sha, data.file).then(text => { this.update(uri); - setTimeout(() => { - let uriString = uri.toString(); - let editor = window.visibleTextEditors.find((e: any) => (e._documentData && e._documentData._uri && e._documentData._uri.toString()) === uriString); - if (editor) { - editor.setDecorations(this._blameDecoration, data.lines.map(l => new Range(l.line, 0, l.line, 1))); - } - }, 1500); + // TODO: This only works on the first load -- not after since it is cached + this._tryAddBlameDecorations(uri, data); + + // TODO: This needs to move to selection somehow to show on the main file editor + //this._addBlameDecorations(editor, data); - // let foo = text.split('\n'); - // return foo.slice(data.range.start.line, data.range.end.line).join('\n') return text; }); @@ -60,18 +78,47 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi // }); // }); } + + private _findEditor(uri: Uri): TextEditor { + let uriString = uri.toString(); + const matcher = (e: any) => (e._documentData && e._documentData._uri && e._documentData._uri.toString()) === uriString; + if (matcher(window.activeTextEditor)) { + return window.activeTextEditor; + } + return window.visibleTextEditors.find(matcher); + } + + private _tryAddBlameDecorations(uri: Uri, data: IGitBlameUriData) { + let handle = setInterval(() => { + let editor = this._findEditor(uri); + if (editor) { + clearInterval(handle); + editor.setDecorations(this._blameDecoration, data.lines.map(l => { + return { + range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)), + hoverMessage: `${moment(l.date).fromNow()}\n${l.author}\n${l.sha}` + }; + })); + } + }, 200); + } + + // private _addBlameDecorations(editor: TextEditor, data: IGitBlameUriData) { + // editor.setDecorations(this._blameDecoration, data.lines.map(l => editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)))); + // } } export interface IGitBlameUriData extends IGitBlameLine { repoPath: string, range: Range, + index: number, lines: IGitBlameLine[] } export function toGitBlameUri(data: IGitBlameUriData) { let ext = extname(data.file); let path = `${dirname(data.file)}/${data.sha}: ${basename(data.file, ext)}${ext}`; - return Uri.parse(`${DocumentSchemes.GitBlame}:${path}?${JSON.stringify(data)}`); + return Uri.parse(`${DocumentSchemes.GitBlame}:${data.index}. ${moment(data.date).format('YYYY-MM-DD hh:MMa')} ${path}?${JSON.stringify(data)}`); } export function fromGitBlameUri(uri: Uri): IGitBlameUriData { diff --git a/src/extension.ts b/src/extension.ts index 0c0f268..7421398 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +19,7 @@ export function activate(context: ExtensionContext) { return commands.executeCommand(VsCodeCommands.ShowReferences, ...args); })); - let selector: DocumentSelector = { scheme: 'file' }; + const selector: DocumentSelector = { scheme: 'file' }; context.subscriptions.push(languages.registerCodeLensProvider(selector, new GitCodeLensProvider(repoPath))); }).catch(reason => console.warn(reason)); } diff --git a/src/git.ts b/src/git.ts index 0ff3acb..18f0d3f 100644 --- a/src/git.ts +++ b/src/git.ts @@ -14,48 +14,54 @@ export declare interface IGitBlameLine { code: string; } -export function gitRepoPath(cwd) { - const mapper = (input, output) => { - output.push(input.toString().replace(/\r?\n|\r/g, '')) - }; - - return new Promise((resolve, reject) => { - gitCommand(cwd, mapper, 'rev-parse', '--show-toplevel') - .then(result => resolve(result[0])) - .catch(reason => reject(reason)); - }); +export function gitRepoPath(cwd): Promise { + let data: Array = []; + const capture = input => data.push(input.toString().replace(/\r?\n|\r/g, '')); + const output = () => data[0]; + + return gitCommand(cwd, capture, output, 'rev-parse', '--show-toplevel'); + + // return new Promise((resolve, reject) => { + // gitCommand(cwd, capture, output, 'rev-parse', '--show-toplevel') + // .then(result => resolve(result[0])) + // .catch(reason => reject(reason)); + // }); } //const blameMatcher = /^(.*)\t\((.*)\t(.*)\t(.*?)\)(.*)$/gm; -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; +//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; +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; export function gitBlame(fileName: string): Promise { - const mapper = (input, output) => { - let blame = input.toString(); - console.log(fileName, 'Blame:', blame); - + let data: string = ''; + const capture = input => data += input.toString(); + const output = () => { + let lines: Array = []; let m: Array; - while ((m = blameMatcher.exec(blame)) != null) { - output.push({ + while ((m = blameMatcher.exec(data)) != null) { + lines.push({ sha: m[1], file: m[2].trim(), - originalLine: parseInt(m[3], 10), + originalLine: parseInt(m[3], 10) - 1, author: m[4].trim(), date: new Date(m[5]), - line: parseInt(m[6], 10), + line: parseInt(m[6], 10) - 1, code: m[7] }); } + return lines; }; - return gitCommand(dirname(fileName), mapper, 'blame', '-fnw', '--', fileName); + return gitCommand(dirname(fileName), capture, output, 'blame', '-fnw', '--', fileName); } export function gitGetVersionFile(repoPath: string, sha: string, source: string): Promise { - const mapper = (input, output) => output.push(input); + let data: Array = []; + const capture = input => data.push(input); + const output = () => data; return new Promise((resolve, reject) => { - (gitCommand(repoPath, mapper, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise>).then(o => { + (gitCommand(repoPath, capture, output, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise>).then(o => { let ext = extname(source); tmp.file({ prefix: `${basename(source, ext)}-${sha}_`, postfix: ext }, (err, destination, fd, cleanupCallback) => { if (err) { @@ -79,19 +85,20 @@ export function gitGetVersionFile(repoPath: string, sha: string, source: string) } export function gitGetVersionText(repoPath: string, sha: string, source: string): Promise { - const mapper = (input, output) => output.push(input.toString()); + let data: Array = []; + const capture = input => data.push(input.toString()); + const output = () => data; - return new Promise((resolve, reject) => (gitCommand(repoPath, mapper, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise>).then(o => resolve(o.join()))); + return new Promise((resolve, reject) => (gitCommand(repoPath, capture, output, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise>).then(o => resolve(o.join()))); } -function gitCommand(cwd: string, mapper: (input: Buffer, output: Array) => void, ...args): Promise { +function gitCommand(cwd: string, capture: (input: Buffer) => void, output: () => any, ...args): Promise { return new Promise((resolve, reject) => { let spawn = require('child_process').spawn; let process = spawn('git', args, { cwd: cwd }); - let output: Array = []; process.stdout.on('data', data => { - mapper(data, output); + capture(data); }); let errors: Array = []; @@ -105,7 +112,11 @@ function gitCommand(cwd: string, mapper: (input: Buffer, output: Array) => return; } - resolve(output); + try { + resolve(output()); + } catch (ex) { + reject(ex); + } }); }); } \ No newline at end of file