From 70cc92ddd61127d70a1ad4adb631980fa55843fa Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 31 Aug 2016 21:15:06 -0400 Subject: [PATCH] Adds CodeLens for Diff'ing in blame Other fixes and refactoring --- package.json | 2 +- src/codeLensProvider.ts | 107 -------------------------------------- src/commands.ts | 30 +++++++++++ src/constants.ts | 10 ++-- src/contentProvider.ts | 110 --------------------------------------- src/extension.ts | 17 +++--- src/git.ts | 7 ++- src/gitBlameCodeLensProvider.ts | 87 +++++++++++++++++++++++++++++++ src/gitBlameContentProvider.ts | 110 +++++++++++++++++++++++++++++++++++++++ src/gitCodeLensProvider.ts | 111 ++++++++++++++++++++++++++++++++++++++++ src/gitProvider.ts | 4 +- 11 files changed, 364 insertions(+), 231 deletions(-) delete mode 100644 src/codeLensProvider.ts delete mode 100644 src/contentProvider.ts create mode 100644 src/gitBlameCodeLensProvider.ts create mode 100644 src/gitBlameContentProvider.ts create mode 100644 src/gitCodeLensProvider.ts diff --git a/package.json b/package.json index 5e50b37..688b75f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlens", - "version": "0.0.2", + "version": "0.0.3", "author": "Eric Amodio", "publisher": "eamodio", "engines": { diff --git a/src/codeLensProvider.ts b/src/codeLensProvider.ts deleted file mode 100644 index 7a8bd27..0000000 --- a/src/codeLensProvider.ts +++ /dev/null @@ -1,107 +0,0 @@ -'use strict'; -import {CancellationToken, CodeLens, CodeLensProvider, commands, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; -import {Commands, VsCodeCommands, WorkspaceState} from './constants'; -import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider'; -import * as moment from 'moment'; - -export class GitBlameCodeLens extends CodeLens { - constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) { - super(range); - } - - getBlame(): Promise { - return this.git.getBlameForRange(this.fileName, this.blameRange); - } -} - -export class GitHistoryCodeLens extends CodeLens { - constructor(public repoPath: string, public fileName: string, range: Range) { - super(range); - } -} - -export default class GitCodeLensProvider implements CodeLensProvider { - constructor(context: ExtensionContext, private git: GitProvider) { } - - provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { - this.git.getBlameForFile(document.fileName); - - return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise).then(symbols => { - let lenses: CodeLens[] = []; - symbols.forEach(sym => this._provideCodeLens(document, sym, 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(this.git, document.fileName, docRange, new Range(0, 0, 0, docRange.start.character))); - lenses.push(new GitHistoryCodeLens(this.git.repoPath, document.fileName, new Range(0, 1, 0, docRange.start.character))); - } - return lenses; - }); - } - - private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void { - switch (symbol.kind) { - case SymbolKind.Package: - case SymbolKind.Module: - case SymbolKind.Class: - case SymbolKind.Interface: - case SymbolKind.Constructor: - case SymbolKind.Method: - case SymbolKind.Property: - case SymbolKind.Field: - case SymbolKind.Function: - case SymbolKind.Enum: - break; - default: - return; - } - - const line = document.lineAt(symbol.location.range.start); - - let startChar = line.text.search(`\\b${symbol.name}\\b`); //line.firstNonWhitespaceCharacterIndex; - if (startChar === -1) { - startChar = line.firstNonWhitespaceCharacterIndex; - } else { - startChar += Math.floor(symbol.name.length / 2); - } - - lenses.push(new GitBlameCodeLens(this.git, document.fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); - lenses.push(new GitHistoryCodeLens(this.git.repoPath, document.fileName, line.range.with(new Position(line.range.start.line, startChar + 1)))); - } - - resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { - if (lens instanceof GitBlameCodeLens) return this._resolveGitBlameCodeLens(lens, token); - if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token); - } - - _resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable { - return new Promise((resolve, reject) => { - lens.getBlame().then(blame => { - if (!blame.lines.length) { - console.error('No blame lines found', lens); - reject(null); - return; - } - - const recentCommit = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime())[0]; - lens.command = { - title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`, - command: Commands.ShowBlameHistory, - arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] - }; - resolve(lens); - }); - });//.catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing - } - - _resolveGitHistoryCodeLens(lens: GitHistoryCodeLens, token: CancellationToken): Thenable { - // TODO: Play with this more -- get this to open the correct diff to the right place - lens.command = { - title: `View History`, - command: 'git.viewFileHistory', // viewLineHistory - arguments: [Uri.file(lens.fileName)] - }; - return Promise.resolve(lens); - } -} diff --git a/src/commands.ts b/src/commands.ts index 5f57868..6e6b85d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,6 +2,7 @@ import {commands, Disposable, Position, Range, Uri, window} from 'vscode'; import {Commands, VsCodeCommands} from './constants'; import GitProvider from './gitProvider'; +import {basename} from 'path'; abstract class Command extends Disposable { private _subscriptions: Disposable; @@ -41,4 +42,33 @@ export class BlameCommand extends Command { return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations); }); } +} + +export class DiffWithPreviousCommand extends Command { + constructor(private git: GitProvider) { + super(Commands.DiffWithPrevious); + } + + execute(uri?: Uri, sha?: string, compareWithSha?: string) { + // TODO: Execute these in parallel rather than series + return this.git.getVersionedFile(uri.path, sha).then(source => { + this.git.getVersionedFile(uri.path, compareWithSha).then(compare => { + const fileName = basename(uri.path); + return commands.executeCommand(VsCodeCommands.Diff, Uri.file(source), Uri.file(compare), `${fileName} (${sha}) ↔ ${fileName} (${compareWithSha})`); + }) + }); + } +} + +export class DiffWithWorkingCommand extends Command { + constructor(private git: GitProvider) { + super(Commands.DiffWithWorking); + } + + execute(uri?: Uri, sha?: string) { + return this.git.getVersionedFile(uri.path, sha).then(compare => { + const fileName = basename(uri.path); + return commands.executeCommand(VsCodeCommands.Diff, uri, Uri.file(compare), `${fileName} (index) ↔ ${fileName} (${sha})`); + }); + } } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 9c02d87..1cbb4b6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,18 +7,22 @@ export const WorkspaceState = { export const RepoPath: string = 'repoPath'; -export type Commands = 'git.action.showBlameHistory'; +export type Commands = 'git.action.diffWithPrevious' | 'git.action.diffWithWorking' | 'git.action.showBlameHistory'; export const Commands = { + DiffWithPrevious: 'git.action.diffWithPrevious' as Commands, + DiffWithWorking: 'git.action.diffWithWorking' as Commands, ShowBlameHistory: 'git.action.showBlameHistory' as Commands } -export type DocumentSchemes = 'gitblame'; +export type DocumentSchemes = 'file' | 'gitblame'; export const DocumentSchemes = { + File: 'file' as DocumentSchemes, GitBlame: 'gitblame' as DocumentSchemes } -export type VsCodeCommands = 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider' | 'editor.action.showReferences'; +export type VsCodeCommands = 'vscode.diff' | 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider' | 'editor.action.showReferences'; export const VsCodeCommands = { + Diff: 'vscode.diff' as VsCodeCommands, ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as VsCodeCommands, ExecuteCodeLensProvider: 'vscode.executeCodeLensProvider' as VsCodeCommands, ShowReferences: 'editor.action.showReferences' as VsCodeCommands diff --git a/src/contentProvider.ts b/src/contentProvider.ts deleted file mode 100644 index 1248d2c..0000000 --- a/src/contentProvider.ts +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; -import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode'; -import {DocumentSchemes, WorkspaceState} from './constants'; -import GitProvider, {IGitBlameUriData} from './gitProvider'; -import * as moment from 'moment'; - -export default class GitBlameContentProvider implements TextDocumentContentProvider { - static scheme = DocumentSchemes.GitBlame; - - private _blameDecoration: TextEditorDecorationType; - private _onDidChange = new EventEmitter(); - //private _subscriptions: Disposable; - - constructor(context: ExtensionContext, private git: GitProvider) { - this._blameDecoration = window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(255, 255, 255, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), - overviewRulerColor: 'rgba(255, 255, 255, 0.75)', - }, - light: { - backgroundColor: 'rgba(0, 0, 0, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-light.png'), - overviewRulerColor: 'rgba(0, 0, 0, 0.75)', - }, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Right, - isWholeLine: true - }); - - //this._subscriptions = Disposable.from( - // window.onDidChangeActiveTextEditor(e => e ? console.log(e.document.uri) : console.log('active missing')), - //); - } - - dispose() { - this._onDidChange.dispose(); - //this._subscriptions && this._subscriptions.dispose(); - } - - get onDidChange() { - return this._onDidChange.event; - } - - public update(uri: Uri) { - this._onDidChange.fire(uri); - } - - provideTextDocumentContent(uri: Uri): string | Thenable { - const data = this.git.fromBlameUri(uri); - - //const editor = this._findEditor(Uri.file(join(data.repoPath, data.file))); - - return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.sha).then(text => { - this.update(uri); - - // 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); - - return text; - }); - - // return this.git.getVersionedFile(data.fileName, data.sha).then(dst => { - // let uri = Uri.parse(`file:${dst}`) - // return workspace.openTextDocument(uri).then(doc => { - // this.update(uri); - // return doc.getText(); - // }); - // }); - } - - private _findEditor(uri: Uri): TextEditor { - let uriString = uri.toString(); - // TODO: This is a big hack :) - const matcher = (e: any) => (e && 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) { - // Needs to be on a timer for some reason because we won't find the editor otherwise -- is there an event? - let handle = setInterval(() => { - let editor = this._findEditor(uri); - if (!editor) return; - - clearInterval(handle); - this.git.getBlameForShaRange(data.fileName, data.sha, data.range).then(blame => { - if (!blame.lines.length) return; - - this.git.getCommitMessage(data.sha).then(msg => { - editor.setDecorations(this._blameDecoration, blame.lines.map(l => { - return { - range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)), - hoverMessage: `${msg}\n${blame.commit.author}\n${moment(blame.commit.date).format('MMMM Do, YYYY hh:MM a')}\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)))); - // } -} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 4a42fa6..fabcbe1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,9 +1,10 @@ 'use strict'; import {CodeLens, DocumentSelector, ExtensionContext, languages, workspace} from 'vscode'; -import GitCodeLensProvider, {GitBlameCodeLens} from './codeLensProvider'; -import GitContentProvider from './contentProvider'; +import GitCodeLensProvider from './gitCodeLensProvider'; +import GitBlameCodeLensProvider from './gitBlameCodeLensProvider'; +import GitBlameContentProvider from './gitBlameContentProvider'; import GitProvider from './gitProvider'; -import {BlameCommand} from './commands'; +import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; import {WorkspaceState} from './constants'; // this method is called when your extension is activated @@ -23,12 +24,12 @@ export function activate(context: ExtensionContext) { git.getRepoPath(workspace.rootPath).then(repoPath => { context.workspaceState.update(WorkspaceState.RepoPath, repoPath); - context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); - + context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git))); + context.subscriptions.push(languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(context, git))); + context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); context.subscriptions.push(new BlameCommand(git)); - - const selector: DocumentSelector = { scheme: 'file' }; - context.subscriptions.push(languages.registerCodeLensProvider(selector, new GitCodeLensProvider(context, git))); + context.subscriptions.push(new DiffWithPreviousCommand(git)); + context.subscriptions.push(new DiffWithWorkingCommand(git)); }).catch(reason => console.warn(reason)); } diff --git a/src/git.ts b/src/git.ts index c9f89f9..f770b6a 100644 --- a/src/git.ts +++ b/src/git.ts @@ -18,9 +18,14 @@ export default class Git { return gitCommand(cwd, 'rev-parse', '--show-toplevel').then(data => data.replace(/\r?\n|\r/g, '')); } - static blame(fileName: string, repoPath: string) { + static blame(fileName: string, repoPath: string, sha?: string) { fileName = Git.normalizePath(fileName, repoPath); + if (sha) { + console.log('git', 'blame', '-fnw', '--root', `${sha}^`, '--', fileName); + return gitCommand(repoPath, 'blame', '-fnw', '--root', `${sha}^`, '--', fileName); + } + console.log('git', 'blame', '-fnw', '--root', '--', fileName); return gitCommand(repoPath, 'blame', '-fnw', '--root', '--', fileName); // .then(s => { console.log(s); return s; }) diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts new file mode 100644 index 0000000..d7b5997 --- /dev/null +++ b/src/gitBlameCodeLensProvider.ts @@ -0,0 +1,87 @@ +'use strict'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; +import {Commands, DocumentSchemes, VsCodeCommands, WorkspaceState} from './constants'; +import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider'; +import {join} from 'path'; +import * as moment from 'moment'; + +export class GitDiffWithWorkingTreeCodeLens extends CodeLens { + constructor(private git: GitProvider, public fileName: string, public sha: string, range: Range) { + super(range); + } +} + +export class GitDiffWithPreviousCodeLens extends CodeLens { + constructor(private git: GitProvider, public fileName: string, public sha: string, public compareWithSha: string, range: Range) { + super(range); + } +} + +export default class GitBlameCodeLensProvider implements CodeLensProvider { + static selector: DocumentSelector = { scheme: DocumentSchemes.GitBlame }; + + constructor(context: ExtensionContext, private git: GitProvider) { } + + provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { + const data = this.git.fromBlameUri(document.uri); + const fileName = data.fileName; + + return this.git.getBlameForFile(fileName).then(blame => { + const commits = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime()); + let index = commits.findIndex(c => c.sha === data.sha) + 1; + + let previousCommit: IGitBlameCommit; + if (index < commits.length) { + previousCommit = commits[index]; + } + + const lenses: CodeLens[] = []; + + // Add codelens to each "group" of blame lines + const lines = blame.lines.filter(l => l.sha === data.sha); + let lastLine = lines[0].originalLine; + lines.forEach(l => { + if (l.originalLine !== lastLine + 1) { + lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, data.sha, new Range(l.originalLine, 0, l.originalLine, 1))); + if (previousCommit) { + lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, data.sha, previousCommit.sha, new Range(l.originalLine, 1, l.originalLine, 2))); + } + } + lastLine = l.originalLine; + }); + + // 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)) { + lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, data.sha, new Range(0, 0, 0, 1))); + if (previousCommit) { + lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, data.sha, previousCommit.sha, new Range(0, 1, 0, 2))); + } + } + + return lenses; + }); + } + + resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { + if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token); + if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token); + } + + _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable { + lens.command = { + title: `Compare with Working Tree`, + command: Commands.DiffWithWorking, + arguments: [Uri.file(join(this.git.repoPath, lens.fileName)), lens.sha] + }; + return Promise.resolve(lens); + } + + _resolveGitDiffWithPreviousCodeLens(lens: GitDiffWithPreviousCodeLens, token: CancellationToken): Thenable { + lens.command = { + title: `Compare with Previous (${lens.compareWithSha})`, + command: Commands.DiffWithPrevious, + arguments: [Uri.file(join(this.git.repoPath, lens.fileName)), lens.sha, lens.compareWithSha] + }; + return Promise.resolve(lens); + } +} diff --git a/src/gitBlameContentProvider.ts b/src/gitBlameContentProvider.ts new file mode 100644 index 0000000..1248d2c --- /dev/null +++ b/src/gitBlameContentProvider.ts @@ -0,0 +1,110 @@ +'use strict'; +import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode'; +import {DocumentSchemes, WorkspaceState} from './constants'; +import GitProvider, {IGitBlameUriData} from './gitProvider'; +import * as moment from 'moment'; + +export default class GitBlameContentProvider implements TextDocumentContentProvider { + static scheme = DocumentSchemes.GitBlame; + + private _blameDecoration: TextEditorDecorationType; + private _onDidChange = new EventEmitter(); + //private _subscriptions: Disposable; + + constructor(context: ExtensionContext, private git: GitProvider) { + this._blameDecoration = window.createTextEditorDecorationType({ + dark: { + backgroundColor: 'rgba(255, 255, 255, 0.15)', + gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), + overviewRulerColor: 'rgba(255, 255, 255, 0.75)', + }, + light: { + backgroundColor: 'rgba(0, 0, 0, 0.15)', + gutterIconPath: context.asAbsolutePath('images/blame-light.png'), + overviewRulerColor: 'rgba(0, 0, 0, 0.75)', + }, + gutterIconSize: 'contain', + overviewRulerLane: OverviewRulerLane.Right, + isWholeLine: true + }); + + //this._subscriptions = Disposable.from( + // window.onDidChangeActiveTextEditor(e => e ? console.log(e.document.uri) : console.log('active missing')), + //); + } + + dispose() { + this._onDidChange.dispose(); + //this._subscriptions && this._subscriptions.dispose(); + } + + get onDidChange() { + return this._onDidChange.event; + } + + public update(uri: Uri) { + this._onDidChange.fire(uri); + } + + provideTextDocumentContent(uri: Uri): string | Thenable { + const data = this.git.fromBlameUri(uri); + + //const editor = this._findEditor(Uri.file(join(data.repoPath, data.file))); + + return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.sha).then(text => { + this.update(uri); + + // 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); + + return text; + }); + + // return this.git.getVersionedFile(data.fileName, data.sha).then(dst => { + // let uri = Uri.parse(`file:${dst}`) + // return workspace.openTextDocument(uri).then(doc => { + // this.update(uri); + // return doc.getText(); + // }); + // }); + } + + private _findEditor(uri: Uri): TextEditor { + let uriString = uri.toString(); + // TODO: This is a big hack :) + const matcher = (e: any) => (e && 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) { + // Needs to be on a timer for some reason because we won't find the editor otherwise -- is there an event? + let handle = setInterval(() => { + let editor = this._findEditor(uri); + if (!editor) return; + + clearInterval(handle); + this.git.getBlameForShaRange(data.fileName, data.sha, data.range).then(blame => { + if (!blame.lines.length) return; + + this.git.getCommitMessage(data.sha).then(msg => { + editor.setDecorations(this._blameDecoration, blame.lines.map(l => { + return { + range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)), + hoverMessage: `${msg}\n${blame.commit.author}\n${moment(blame.commit.date).format('MMMM Do, YYYY hh:MM a')}\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)))); + // } +} \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts new file mode 100644 index 0000000..6e5e15d --- /dev/null +++ b/src/gitCodeLensProvider.ts @@ -0,0 +1,111 @@ +'use strict'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; +import {Commands, DocumentSchemes, VsCodeCommands, WorkspaceState} from './constants'; +import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider'; +import * as moment from 'moment'; + +export class GitCodeLens extends CodeLens { + constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) { + super(range); + } + + getBlame(): Promise { + return this.git.getBlameForRange(this.fileName, this.blameRange); + } +} + +export class GitHistoryCodeLens extends CodeLens { + constructor(public repoPath: string, public fileName: string, range: Range) { + super(range); + } +} + +export default class GitCodeLensProvider implements CodeLensProvider { + static selector: DocumentSelector = { scheme: DocumentSchemes.File }; + + constructor(context: ExtensionContext, private git: GitProvider) { } + + provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { + const fileName = document.fileName; + + this.git.getBlameForFile(fileName); + + return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise).then(symbols => { + const lenses: CodeLens[] = []; + symbols.forEach(sym => this._provideCodeLens(fileName, document, sym, 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 blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); + lenses.push(new GitCodeLens(this.git, fileName, blameRange, new Range(0, 0, 0, blameRange.start.character))); + lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, new Range(0, 1, 0, blameRange.start.character))); + } + return lenses; + }); + } + + private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void { + switch (symbol.kind) { + case SymbolKind.Package: + case SymbolKind.Module: + case SymbolKind.Class: + case SymbolKind.Interface: + case SymbolKind.Constructor: + case SymbolKind.Method: + case SymbolKind.Property: + case SymbolKind.Field: + case SymbolKind.Function: + case SymbolKind.Enum: + break; + default: + return; + } + + const line = document.lineAt(symbol.location.range.start); + + let startChar = line.text.search(`\\b${symbol.name}\\b`); //line.firstNonWhitespaceCharacterIndex; + if (startChar === -1) { + startChar = line.firstNonWhitespaceCharacterIndex; + } else { + startChar += Math.floor(symbol.name.length / 2); + } + + lenses.push(new GitCodeLens(this.git, fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, line.range.with(new Position(line.range.start.line, startChar + 1)))); + } + + resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { + if (lens instanceof GitCodeLens) return this._resolveGitBlameCodeLens(lens, token); + if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token); + } + + _resolveGitBlameCodeLens(lens: GitCodeLens, token: CancellationToken): Thenable { + return new Promise((resolve, reject) => { + lens.getBlame().then(blame => { + if (!blame.lines.length) { + console.error('No blame lines found', lens); + reject(null); + return; + } + + const recentCommit = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime())[0]; + lens.command = { + title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`, + command: Commands.ShowBlameHistory, + arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] + }; + resolve(lens); + }); + });//.catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing + } + + _resolveGitHistoryCodeLens(lens: GitHistoryCodeLens, token: CancellationToken): Thenable { + // TODO: Play with this more -- get this to open the correct diff to the right place + lens.command = { + title: `View History`, + command: 'git.viewFileHistory', // viewLineHistory + arguments: [Uri.file(lens.fileName)] + }; + return Promise.resolve(lens); + } +} \ No newline at end of file diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 9e2ca57..b23a069 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -151,7 +151,7 @@ export default class GitProvider extends Disposable { return Uri.parse(`${DocumentSchemes.GitBlame}:${pad(index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MM a')} - ${path}?${JSON.stringify(data)}`); } - fromBlameUri(uri: Uri) { + fromBlameUri(uri: Uri): IGitBlameUriData { const data = JSON.parse(uri.query); data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character); return data; @@ -169,6 +169,7 @@ export interface IGitBlameCommit { author: string; date: Date; } + export interface IGitBlameLine { sha: string; line: number; @@ -176,6 +177,7 @@ export interface IGitBlameLine { originalFileName?: string; code?: string; } + export interface IGitBlameUriData { fileName: string, originalFileName?: string;