diff --git a/package.json b/package.json index 0816d58..bb006a9 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "main": "./out/src/extension", "contributes": { "commands": [{ - "command": "git.action.showBlameHistory", - "title": "Show Blame History", + "command": "git.action.showBlame", + "title": "Show Git Blame", "category": "Git" }] }, @@ -38,12 +38,12 @@ "tmp": "^0.0.28" }, "devDependencies": { - "typescript": "^1.8.10", + "typescript": "^2.0.0", "vscode": "^0.11.17" }, "scripts": { "vscode:prepublish": "node ./node_modules/vscode/bin/compile", "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install && tsc" + "postinstall": "node ./node_modules/vscode/bin/install" } } \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index de4f83f..2060860 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -23,7 +23,7 @@ abstract class Command extends Disposable { export class BlameCommand extends Command { constructor(private git: GitProvider, private blameController: GitBlameController) { - super(Commands.ShowBlameHistory); + super(Commands.ShowBlame); } execute(uri?: Uri, range?: Range, sha?: string) { @@ -44,204 +44,29 @@ export class BlameCommand extends Command { } } -// export class BlameCommand extends Command { -// // static Colors: Array> = [ [255, 152, 0], [255, 87, 34], [121, 85, 72], [158, 158, 158], [96, 125, 139], [244, 67, 54], [233, 30, 99], [156, 39, 176], [103, 58, 183] ]; -// // private _decorations: TextEditorDecorationType[] = []; +export class HistoryCommand extends Command { + constructor(private git: GitProvider) { + super(Commands.ShowHistory); + } -// constructor(private git: GitProvider, private blameDecoration: TextEditorDecorationType, private highlightDecoration: TextEditorDecorationType) { -// super(Commands.ShowBlameHistory); + execute(uri?: Uri, range?: Range, position?: Position) { + // If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file) + if (!uri) { + const doc = window.activeTextEditor && window.activeTextEditor.document; + if (doc) { + uri = doc.uri; + range = doc.validateRange(new Range(0, 0, 1000000, 1000000)); + position = doc.validateRange(new Range(0, 0, 0, 1000000)).start; + } -// // BlameCommand.Colors.forEach(c => { -// // this._decorations.push(window.createTextEditorDecorationType({ -// // dark: { -// // backgroundColor: `rgba(${c[0]}, ${c[1]}, ${c[2]}, 0.15)`, -// // //gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), -// // overviewRulerColor: `rgba(${c[0]}, ${c[1]}, ${c[2]}, 0.75)`, -// // }, -// // //light: { -// // //backgroundColor: 'rgba(0, 0, 0, 0.15)', -// // //gutterIconPath: context.asAbsolutePath('images/blame-light.png'), -// // //overviewRulerColor: c //'rgba(0, 0, 0, 0.75)', -// // //}, -// // // before: { -// // // margin: '0 1em 0 0' -// // // }, -// // // after: { -// // // margin: '0 0 0 2em' -// // // }, -// // //gutterIconSize: 'contain', -// // overviewRulerLane: OverviewRulerLane.Right, -// // //isWholeLine: true -// // })); -// // }); -// } + if (!uri) return; + } -// execute(uri?: Uri, range?: Range, position?: Position) { -// const editor = window.activeTextEditor; -// if (!editor) { -// return; -// } - -// editor.setDecorations(this.blameDecoration, []); -// editor.setDecorations(this.highlightDecoration, []); - -// const highlightDecorationRanges: Array = []; -// const blameDecorationOptions: Array = []; - -// this.git.getBlameForRange(uri.path, range).then(blame => { -// if (!blame.lines.length) return; - -// const commits = Array.from(blame.commits.values()); -// const recentCommit = commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0]; - -// return this.git.getCommitMessages(uri.path) -// .then(msgs => { -// commits.forEach(c => { -// c.message = msgs.get(c.sha.substring(0, c.sha.length - 1)); -// }); - -// blame.lines.forEach(l => { -// if (l.sha === recentCommit.sha) { -// highlightDecorationRanges.push(editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); -// } - -// const c = blame.commits.get(l.sha); -// blameDecorationOptions.push({ -// range: editor.document.validateRange(new Range(l.line, 0, l.line, 0)), -// hoverMessage: `${c.sha}: ${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, -// renderOptions: { -// // dark: { -// // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})` -// // }, -// before: { -// //border: '1px solid gray', -// //color: 'rgb(128, 128, 128)', -// contentText: `${l.sha}`, -// // margin: '0 1em 0 0', -// // width: '5em' -// } -// // after: { -// // contentText: `${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, -// // //color: 'rbg(128, 128, 128)', -// // margin: '0 0 0 2em' -// // } -// } -// }); -// }); -// }); - -// // Array.from(blame.commits.values()).forEach((c, i) => { -// // if (i == 0) { -// // highlightDecorationRanges = blame.lines -// // .filter(l => l.sha === c.sha) -// // .map(l => editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); -// // } - -// // blameDecorationOptions.push(blame.lines -// // .filter(l => l.sha === c.sha) -// // .map(l => { -// // return { -// // range: editor.document.validateRange(new Range(l.line, 0, l.line, 6)), -// // hoverMessage: `${c.author}\n${moment(c.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`, -// // renderOptions: { -// // // dark: { -// // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})` -// // // }, -// // before: { -// // //border: '1px solid gray', -// // //color: 'rgb(128, 128, 128)', -// // contentText: `${l.sha}`, -// // // margin: '0 1em 0 0', -// // // width: '5em' -// // } -// // // after: { -// // // contentText: `${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, -// // // //color: 'rbg(128, 128, 128)', -// // // margin: '0 0 0 2em' -// // // } -// // } -// // }; -// // })); -// // }); -// }) -// .then(() => { -// editor.setDecorations(this.blameDecoration, blameDecorationOptions); -// editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); -// }); - -// // this._decorations.forEach(d => editor.setDecorations(d, [])); -// // this.git.getBlameForRange(uri.path, range).then(blame => { -// // if (!blame.lines.length) return; - -// // Array.from(blame.commits.values()).forEach((c, i) => { -// // editor.setDecorations(this._decorations[i], blame.lines.filter(l => l.sha === c.sha).map(l => { -// // const commit = c; //blame.commits.get(l.sha); -// // return { -// // range: editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)), -// // hoverMessage: `${commit.author}\n${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`, -// // renderOptions: { -// // // dark: { -// // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})` -// // // }, -// // before: { -// // color: 'rgb(128, 128, 128)', -// // contentText: `${l.sha}`, -// // //border: '1px solid gray', -// // width: '5em', -// // margin: '0 1em 0 0' -// // }, -// // after: { -// // contentText: `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`, -// // //color: 'rbg(128, 128, 128)', -// // margin: '0 0 0 2em' -// // } -// // } -// // }; -// // })); -// // }); - -// // //this.git.getCommitMessage(data.sha).then(msg => { -// // // editor.setDecorations(this._blameDecoration, blame.lines.map(l => { -// // // const commit = blame.commits.get(l.sha); -// // // return { -// // // range: editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)), -// // // hoverMessage: `${commit.author}\n${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`, -// // // renderOptions: { -// // // // dark: { -// // // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})` -// // // // }, -// // // before: { -// // // contentText: `${l.sha}`, -// // // margin: '0 0 0 -10px' -// // // }, -// // // after: { -// // // contentText: `${l.sha}`, -// // // color: 'rbg(128, 128, 128)', -// // // margin: '0 20px 0 0' -// // // } -// // // } -// // // }; -// // // })); -// // //}) -// // }); - -// // // If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file) -// // if (!uri) { -// // const doc = window.activeTextEditor && window.activeTextEditor.document; -// // if (doc) { -// // uri = doc.uri; -// // range = doc.validateRange(new Range(0, 0, 1000000, 1000000)); -// // position = doc.validateRange(new Range(0, 0, 0, 1000000)).start; -// // } - -// // if (!uri) return; -// // } - -// // return this.git.getBlameLocations(uri.path, range).then(locations => { -// // return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations); -// // }); -// } -// } + return this.git.getBlameLocations(uri.path, range).then(locations => { + return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations); + }); + } +} export class DiffWithPreviousCommand extends Command { constructor(private git: GitProvider) { diff --git a/src/constants.ts b/src/constants.ts index 203d0d9..e1b4bdc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,16 +8,18 @@ export const WorkspaceState = { export const RepoPath: string = 'repoPath'; -export type Commands = 'git.action.diffWithPrevious' | 'git.action.diffWithWorking' | 'git.action.showBlameHistory'; +export type Commands = 'git.action.diffWithPrevious' | 'git.action.diffWithWorking' | 'git.action.showBlame' | 'git.action.showHistory'; export const Commands = { DiffWithPrevious: 'git.action.diffWithPrevious' as Commands, DiffWithWorking: 'git.action.diffWithWorking' as Commands, - ShowBlameHistory: 'git.action.showBlameHistory' as Commands + ShowBlame: 'git.action.showBlame' as Commands, + ShowHistory: 'git.action.showHistory' as Commands, } -export type DocumentSchemes = 'file' | 'gitblame'; +export type DocumentSchemes = 'file' | 'git' | 'gitblame'; export const DocumentSchemes = { File: 'file' as DocumentSchemes, + Git: 'git' as DocumentSchemes, GitBlame: 'gitblame' as DocumentSchemes } diff --git a/src/extension.ts b/src/extension.ts index 8e6ff10..0b70ca8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,12 @@ 'use strict'; import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, OverviewRulerLane, window, workspace} from 'vscode'; import GitCodeLensProvider from './gitCodeLensProvider'; +import GitContentProvider from './gitContentProvider'; import GitBlameCodeLensProvider from './gitBlameCodeLensProvider'; import GitBlameContentProvider from './gitBlameContentProvider'; import GitBlameController from './gitBlameController'; import GitProvider from './gitProvider'; -import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; +import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand, HistoryCommand} from './commands'; import {WorkspaceState} from './constants'; // this method is called when your extension is activated @@ -26,7 +27,9 @@ export function activate(context: ExtensionContext) { context.workspaceState.update(WorkspaceState.RepoPath, repoPath); context.workspaceState.update(WorkspaceState.HasGitHistoryExtension, extensions.getExtension('donjayamanne.githistory') !== undefined); + 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))); @@ -34,6 +37,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(blameController); context.subscriptions.push(new BlameCommand(git, blameController)); + context.subscriptions.push(new HistoryCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git)); context.subscriptions.push(new DiffWithWorkingCommand(git)); }).catch(reason => console.warn(reason)); diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts index 0f8af0b..3ec5cfa 100644 --- a/src/gitBlameCodeLensProvider.ts +++ b/src/gitBlameCodeLensProvider.ts @@ -1,7 +1,7 @@ '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 GitProvider, {IGitBlame, IGitCommit} from './gitProvider'; import {join} from 'path'; import * as moment from 'moment'; @@ -25,12 +25,13 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider { provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { const data = this.git.fromBlameUri(document.uri); const fileName = data.fileName; + const sha = data.sha; 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; + const commits = Array.from(blame.commits.values()); + let index = commits.findIndex(c => c.sha === sha) + 1; - let previousCommit: IGitBlameCommit; + let previousCommit: IGitCommit; if (index < commits.length) { previousCommit = commits[index]; } @@ -38,13 +39,13 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider { const lenses: CodeLens[] = []; // Add codelens to each "group" of blame lines - const lines = blame.lines.filter(l => l.sha === data.sha && l.originalLine >= data.range.start.line && l.originalLine <= data.range.end.line); + const lines = blame.lines.filter(l => l.sha === sha && l.originalLine >= data.range.start.line && l.originalLine <= data.range.end.line); 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))); + lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, 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))); + lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, sha, previousCommit.sha, new Range(l.originalLine, 1, l.originalLine, 2))); } } lastLine = l.originalLine; @@ -52,9 +53,9 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider { // 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))); + lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, fileName, 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))); + lenses.push(new GitDiffWithPreviousCodeLens(this.git, fileName, sha, previousCommit.sha, new Range(0, 1, 0, 2))); } } diff --git a/src/gitBlameController.ts b/src/gitBlameController.ts index 9a3611b..03f7135 100644 --- a/src/gitBlameController.ts +++ b/src/gitBlameController.ts @@ -1,6 +1,6 @@ 'use strict' -import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Position, Range, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode'; -import {Commands, VsCodeCommands} from './constants'; +import {commands, DecorationOptions, Disposable, ExtensionContext, languages, OverviewRulerLane, Position, Range, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode'; +import {Commands, DocumentSchemes, VsCodeCommands} from './constants'; import GitProvider, {IGitBlame} from './gitProvider'; import {basename} from 'path'; import * as moment from 'moment'; @@ -9,13 +9,13 @@ export default class GitBlameController extends Disposable { private _controller: GitBlameEditorController; private _subscription: Disposable; - private blameDecoration: TextEditorDecorationType; - private highlightDecoration: TextEditorDecorationType; + private _blameDecoration: TextEditorDecorationType; + private _highlightDecoration: TextEditorDecorationType; constructor(context: ExtensionContext, private git: GitProvider) { super(() => this.dispose()); - this.blameDecoration = window.createTextEditorDecorationType({ + this._blameDecoration = window.createTextEditorDecorationType({ before: { color: '#5a5a5a', margin: '0 1em 0 0', @@ -23,7 +23,7 @@ export default class GitBlameController extends Disposable { }, }); - this.highlightDecoration= window.createTextEditorDecorationType({ + this._highlightDecoration= window.createTextEditorDecorationType({ dark: { backgroundColor: 'rgba(255, 255, 255, 0.15)', gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), @@ -68,7 +68,7 @@ export default class GitBlameController extends Disposable { return; } - this._controller = new GitBlameEditorController(this.git, this.blameDecoration, this.highlightDecoration, editor, sha); + this._controller = new GitBlameEditorController(this.git, this._blameDecoration, this._highlightDecoration, editor, sha); return this._controller.applyBlame(sha); } } @@ -120,7 +120,7 @@ class GitBlameEditorController extends Disposable { }); this.editor.setDecorations(this.blameDecoration, blameDecorationOptions); - return this.applyHighlight(sha || commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0].sha); + return this.applyHighlight(sha || commits[0].sha); }); }); } @@ -137,49 +137,4 @@ class GitBlameEditorController extends Disposable { this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); }); } - - // execute(sha?: string) { - // const editor = this.editor; - // const uri = editor.document.uri; - // const range = this.range; - - // // editor.setDecorations(this.blameDecoration, []); - // // editor.setDecorations(this.highlightDecoration, []); - - // const highlightDecorationRanges: Array = []; - // const blameDecorationOptions: Array = []; - - // this.git.getBlameForRange(uri.path, range).then(blame => { - // if (!blame.lines.length) return; - - // const commits = Array.from(blame.commits.values()); - // if (!sha) { - // sha = commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0].sha; - // } - - // return this.git.getCommitMessages(uri.path) - // .then(msgs => { - // commits.forEach(c => { - // c.message = msgs.get(c.sha.substring(0, c.sha.length - 1)); - // }); - - // blame.lines.forEach(l => { - // if (l.sha === sha) { - // highlightDecorationRanges.push(editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); - // } - - // const c = blame.commits.get(l.sha); - // blameDecorationOptions.push({ - // range: editor.document.validateRange(new Range(l.line, 0, l.line, 0)), - // hoverMessage: `${c.sha}: ${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, - // renderOptions: { before: { contentText: `${l.sha}`, } } - // }); - // }); - // }); - // }) - // .then(() => { - // editor.setDecorations(this.blameDecoration, blameDecorationOptions); - // editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); - // }); - // } } \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 65bbc65..958d97d 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,10 +1,20 @@ '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 GitProvider, {IGitBlame, IGitCommit} from './gitProvider'; import * as moment from 'moment'; -export class GitCodeLens extends CodeLens { +export class GitRecentChangeCodeLens 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 GitBlameCodeLens extends CodeLens { public sha: string; constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) { @@ -33,26 +43,32 @@ export default class GitCodeLensProvider implements CodeLensProvider { provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { const fileName = document.fileName; + const promise = Promise.all([this.git.getBlameForFile(fileName) as Promise, (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise)]); - this.git.getBlameForFile(fileName); + return promise.then(values => { + const blame = values[0] as IGitBlame; + if (!blame || !blame.lines.length) return []; - return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise).then(symbols => { + const symbols = values[1] as SymbolInformation[]; 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))); - if (this.hasGitHistoryExtension) { - lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, new Range(0, 1, 0, blameRange.start.character))); - } + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, blameRange, new Range(0, 0, 0, blameRange.start.character))); + lenses.push(new GitBlameCodeLens(this.git, fileName, blameRange, new Range(0, 1, 0, blameRange.start.character))); + // if (this.hasGitHistoryExtension) { + // 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 { + let multiline = false; switch (symbol.kind) { case SymbolKind.Package: case SymbolKind.Module: @@ -60,10 +76,15 @@ export default class GitCodeLensProvider implements CodeLensProvider { case SymbolKind.Interface: case SymbolKind.Constructor: case SymbolKind.Method: - case SymbolKind.Property: - case SymbolKind.Field: case SymbolKind.Function: case SymbolKind.Enum: + multiline = true; + break; + case SymbolKind.Property: + multiline = (symbol.location.range.end.line - symbol.location.range.start.line) > 1; + break; + case SymbolKind.Field: + multiline = false; break; default: return; @@ -81,36 +102,45 @@ export default class GitCodeLensProvider implements CodeLensProvider { 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)))); - if (this.hasGitHistoryExtension) { - lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, line.range.with(new Position(line.range.start.line, startChar + 1)))); + lenses.push(new GitRecentChangeCodeLens(this.git, fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + startChar++; + if (multiline) { + lenses.push(new GitBlameCodeLens(this.git, fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, startChar)))); + startChar++; } + // if (this.hasGitHistoryExtension) { + // lenses.push(new GitHistoryCodeLens(this.git.repoPath, fileName, line.range.with(new Position(line.range.start.line, startChar)))); + // } } resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { - if (lens instanceof GitCodeLens) return this._resolveGitBlameCodeLens(lens, token); + if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token); + if (lens instanceof GitBlameCodeLens) 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; - } + _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable { + return lens.getBlame().then(blame => { + const recentCommit = blame.commits.values().next().value; + lens.command = { + title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`, + command: Commands.ShowHistory, + arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] + }; + return lens; + }); + } - const recentCommit = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime())[0]; - lens.sha = recentCommit.sha; - 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.sha] - }; - resolve(lens); - }); - });//.catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing + _resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable { + return lens.getBlame().then(blame => { + const count = blame.authors.size; + lens.command = { + title: `${count} ${count > 1 ? 'authors' : 'author'} (${blame.authors.values().next().value.name}${count > 1 ? ' and others' : ''})`, + command: Commands.ShowBlame, + arguments: [Uri.file(lens.fileName), lens.blameRange, lens.sha] + }; + return lens; + }); } _resolveGitHistoryCodeLens(lens: GitHistoryCodeLens, token: CancellationToken): Thenable { diff --git a/src/gitContentProvider.ts b/src/gitContentProvider.ts new file mode 100644 index 0000000..49abbbd --- /dev/null +++ b/src/gitContentProvider.ts @@ -0,0 +1,15 @@ +'use strict'; +import {ExtensionContext, TextDocumentContentProvider, Uri} from 'vscode'; +import {DocumentSchemes} from './constants'; +import GitProvider from './gitProvider'; + +export default class GitContentProvider implements TextDocumentContentProvider { + static scheme = DocumentSchemes.Git; + + constructor(context: ExtensionContext, private git: GitProvider) { } + + provideTextDocumentContent(uri: Uri): string | Thenable { + const data = this.git.fromGitUri(uri); + return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.sha); + } +} \ No newline at end of file diff --git a/src/gitProvider.ts b/src/gitProvider.ts index a5defc8..6c570f7 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -21,8 +21,12 @@ export default class GitProvider extends Disposable { this.repoPath = context.workspaceState.get(WorkspaceState.RepoPath) as string; this._blames = new Map(); - this._subscription = Disposable.from(workspace.onDidCloseTextDocument(d => this._removeFile(d.fileName)), - workspace.onDidChangeTextDocument(e => this._removeFile(e.document.fileName))); + this._subscription = Disposable.from( + workspace.onDidCloseTextDocument(d => this._removeFile(d.fileName)), + // TODO: Need a way to reset codelens in response to a save + workspace.onDidSaveTextDocument(d => this._removeFile(d.fileName)) + //workspace.onDidChangeTextDocument(e => this._removeFile(e.document.fileName)) + ); } dispose() { @@ -31,6 +35,7 @@ export default class GitProvider extends Disposable { } private _removeFile(fileName: string) { + fileName = Git.normalizePath(fileName, this.repoPath); this._blames.delete(fileName); } @@ -46,48 +51,81 @@ export default class GitProvider extends Disposable { blame = Git.blame(fileName, this.repoPath) .then(data => { - const commits: Map = new Map(); - const lines: Array = []; + const authors: Map = new Map(); + const commits: Map = new Map(); + const lines: Array = []; + let m: Array; while ((m = blameMatcher.exec(data)) != null) { - let sha = m[1]; + const authorName = m[4].trim(); + let author = authors.get(authorName); + if (!author) { + author = { + name: authorName, + lineCount: 0 + }; + authors.set(authorName, author); + } - if (!commits.has(sha)) { - commits.set(sha, { + const sha = m[1]; + let commit = commits.get(sha); + if (!commit) { + commit = { sha, fileName: fileName, author: m[4].trim(), - date: new Date(m[5]) - }); + date: new Date(m[5]), + lines: [] + }; + commits.set(sha, commit); } - const line: IGitBlameLine = { + const line: IGitCommitLine = { sha, line: parseInt(m[6], 10) - 1, - originalLine: parseInt(m[3], 10) - 1, + originalLine: parseInt(m[3], 10) - 1 //code: m[7] } - let file = m[2].trim(); + const file = m[2].trim(); if (!fileName.toLowerCase().endsWith(file.toLowerCase())) { line.originalFileName = file; } + commit.lines.push(line); lines.push(line); } - return { commits, lines }; + commits.forEach(c => authors.get(c.author).lineCount += c.lines.length); + + const sortedAuthors: Map = new Map(); + const values = Array.from(authors.values()) + .sort((a, b) => b.lineCount - a.lineCount) + .forEach(a => sortedAuthors.set(a.name, a)); + + const sortedCommits = new Map(); + Array.from(commits.values()) + .sort((a, b) => b.date.getTime() - a.date.getTime()) + .forEach(c => sortedCommits.set(c.sha, c)); + + return { + authors: sortedAuthors, + commits: sortedCommits, + lines: lines + }; }); this._blames.set(fileName, blame); return blame; } - getBlameForLine(fileName: string, line: number): Promise<{commit: IGitBlameCommit, line: IGitBlameLine}> { + getBlameForLine(fileName: string, line: number): Promise { return this.getBlameForFile(fileName).then(blame => { const blameLine = blame.lines[line]; + const commit = blame.commits.get(blameLine.sha); return { - commit: blame.commits.get(blameLine.sha), + author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), + commit: commit, line: blameLine }; }); @@ -97,19 +135,51 @@ export default class GitProvider extends Disposable { return this.getBlameForFile(fileName).then(blame => { if (!blame.lines.length) return blame; - const lines = blame.lines.slice(range.start.line, range.end.line + 1); - const commits = new Map(); - _.uniqBy(lines, 'sha').forEach(l => commits.set(l.sha, blame.commits.get(l.sha))); + if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { + return blame; + } - return { commits, lines }; + const lines = blame.lines.slice(range.start.line, range.end.line + 1); + const shas: Set = new Set(); + lines.forEach(l => shas.add(l.sha)); + + const authors: Map = new Map(); + const commits: Map = new Map(); + blame.commits.forEach(c => { + if (!shas.has(c.sha)) return; + + const commit: IGitCommit = Object.assign({}, c, { lines: c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line) }); + commits.set(c.sha, commit); + + let author = authors.get(commit.author); + if (!author) { + author = { + name: commit.author, + lineCount: 0 + }; + authors.set(author.name, author); + } + + author.lineCount += commit.lines.length; + }); + + const sortedAuthors = new Map(); + Array.from(authors.values()) + .sort((a, b) => b.lineCount - a.lineCount) + .forEach(a => sortedAuthors.set(a.name, a)); + + return { authors: sortedAuthors, commits, lines }; }); } - getBlameForShaRange(fileName: string, sha: string, range: Range): Promise<{commit: IGitBlameCommit, lines: IGitBlameLine[]}> { + getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { return this.getBlameForFile(fileName).then(blame => { + const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha); + const commit = Object.assign({}, blame.commits.get(sha), { lines: lines }); return { - commit: blame.commits.get(sha), - lines: blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha) + author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), + commit: commit, + lines: lines }; }); } @@ -120,15 +190,12 @@ export default class GitProvider extends Disposable { const locations: Array = []; Array.from(blame.commits.values()) - .sort((a, b) => b.date.getTime() - a.date.getTime()) .forEach((c, i) => { - const uri = this.toBlameUri(c, range, i + 1, commitCount); - blame.lines - .filter(l => l.sha === c.sha) - .forEach(l => locations.push(new Location(l.originalFileName - ? this.toBlameUri(c, range, i + 1, commitCount, l.originalFileName) - : uri, - new Position(l.originalLine, 0)))); + const uri = this.toBlameUri(c, i + 1, commitCount, range); + c.lines.forEach(l => locations.push(new Location(l.originalFileName + ? this.toBlameUri(c, i + 1, commitCount, range, l.originalFileName) + : uri, + new Position(l.originalLine, 0)))); }); return locations; @@ -159,41 +226,88 @@ export default class GitProvider extends Disposable { return Git.getVersionedFileText(fileName, this.repoPath, sha); } - toBlameUri(commit: IGitBlameCommit, range: Range, index: number, commitCount: number, originalFileName?: string) { - const pad = n => ("0000000" + n).slice(-("" + commitCount).length); + fromBlameUri(uri: Uri): IGitBlameUriData { + if (uri.scheme !== DocumentSchemes.GitBlame) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); + const data = this._fromGitUri(uri); + data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character); + return data; + } + fromGitUri(uri: Uri) { + if (uri.scheme !== DocumentSchemes.Git) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); + return this._fromGitUri(uri); + } + + private _fromGitUri(uri: Uri): T { + return JSON.parse(uri.query) as T; + } + + toBlameUri(commit: IGitCommit, index: number, commitCount: number, range: Range, originalFileName?: string) { + return this._toGitUri(DocumentSchemes.GitBlame, commit, commitCount, this._toGitBlameUriData(commit, index, range, originalFileName)); + } + + toGitUri(commit: IGitCommit, index: number, commitCount: number, originalFileName?: string) { + return this._toGitUri(DocumentSchemes.Git, commit, commitCount, this._toGitUriData(commit, index, originalFileName)); + } + + private _toGitUri(scheme: DocumentSchemes, commit: IGitCommit, commitCount: number, data: IGitUriData | IGitBlameUriData) { + const pad = n => ("0000000" + n).slice(-("" + commitCount).length); + const ext = extname(data.fileName); + const path = `${dirname(data.fileName)}/${commit.sha}: ${basename(data.fileName, ext)}${ext}`; + + // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location + return Uri.parse(`${scheme}:${pad(data.index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MM a')} - ${path}?${JSON.stringify(data)}`); + } + + private _toGitUriData(commit: IGitCommit, index: number, originalFileName?: string): T { const fileName = originalFileName || commit.fileName; - const ext = extname(fileName); - const path = `${dirname(fileName)}/${commit.sha}: ${basename(fileName, ext)}${ext}`; - const data: IGitBlameUriData = { fileName: commit.fileName, sha: commit.sha, range: range, index: index }; + const data = { fileName: commit.fileName, sha: commit.sha, index: index } as T; if (originalFileName) { data.originalFileName = originalFileName; } - // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location - return Uri.parse(`${DocumentSchemes.GitBlame}:${pad(index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MM a')} - ${path}?${JSON.stringify(data)}`); + return data; } - 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); + private _toGitBlameUriData(commit: IGitCommit, index: number, range: Range, originalFileName?: string) { + const data = this._toGitUriData(commit, index, originalFileName); + data.range = range; return data; } } export interface IGitBlame { - commits: Map; - lines: IGitBlameLine[]; + authors: Map; + commits: Map; + lines: IGitCommitLine[]; } -export interface IGitBlameCommit { +export interface IGitBlameLine { + author: IGitAuthor; + commit: IGitCommit; + line: IGitCommitLine; +} + +export interface IGitBlameLines { + author: IGitAuthor; + commit: IGitCommit; + lines: IGitCommitLine[]; +} + +export interface IGitAuthor { + name: string; + lineCount: number; +} + +export interface IGitCommit { sha: string; fileName: string; author: string; date: Date; + lines: IGitCommitLine[]; message?: string; } -export interface IGitBlameLine { +export interface IGitCommitLine { sha: string; line: number; originalLine: number; @@ -201,10 +315,13 @@ export interface IGitBlameLine { code?: string; } -export interface IGitBlameUriData { +export interface IGitUriData { fileName: string, originalFileName?: string; sha: string, - range: Range, index: number +} + +export interface IGitBlameUriData extends IGitUriData { + range: Range } \ No newline at end of file