diff --git a/package.json b/package.json index bb006a9..85a234b 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,47 @@ "main": "./out/src/extension", "contributes": { "commands": [{ - "command": "git.action.showBlame", - "title": "Show Git Blame", - "category": "Git" - }] + "command": "gitlens.showBlame", + "title": "GitLens: Show Git Blame", + "category": "GitLens" + }, + { + "command": "gitlens.toggleBlame", + "title": "GitLens: Toggle Git Blame", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithPrevious", + "title": "GitLens: Diff Commit with Previous", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithWorking", + "title": "GitLens: Diff Commit with Working Tree", + "category": "GitLens" + }], + "menus": { + "editor/title": [{ + "when": "editorTextFocus", + "command": "gitlens.toggleBlame", + "group": "gitlens" + }], + "editor/context": [{ + "when": "editorTextFocus", + "command": "gitlens.toggleBlame", + "group": "gitlens@1.0" + }, + { + "when": "editorTextFocus && editorHasCodeActionsProvider", + "command": "gitlens.diffWithWorking", + "group": "gitlens.blame@1.1" + }, + { + "when": "editorTextFocus && editorHasCodeActionsProvider", + "command": "gitlens.diffWithPrevious", + "group": "gitlens.blame@1.2" + }] + } }, "activationEvents": [ "*" diff --git a/src/commands.ts b/src/commands.ts index 2060860..cfa35fc 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,5 +1,5 @@ 'use strict' -import {commands, DecorationOptions, Disposable, OverviewRulerLane, Position, Range, TextEditorDecorationType, Uri, window} from 'vscode'; +import {commands, DecorationOptions, Disposable, OverviewRulerLane, Position, Range, TextEditor, TextEditorEdit, TextEditorDecorationType, Uri, window} from 'vscode'; import {Commands, VsCodeCommands} from './constants'; import GitProvider from './gitProvider'; import GitBlameController from './gitBlameController'; @@ -11,7 +11,7 @@ abstract class Command extends Disposable { constructor(command: Commands) { super(() => this.dispose()); - this._subscriptions = commands.registerCommand(command, this.execute.bind(this)); + this._subscriptions = commands.registerCommand(command, this.execute, this); } dispose() { @@ -21,19 +21,43 @@ abstract class Command extends Disposable { abstract execute(...args): any; } -export class BlameCommand extends Command { +abstract class EditorCommand extends Disposable { + private _subscriptions: Disposable; + + constructor(command: Commands) { + super(() => this.dispose()); + this._subscriptions = commands.registerTextEditorCommand(command, this.execute, this); + } + + dispose() { + this._subscriptions && this._subscriptions.dispose(); + } + + abstract execute(editor: TextEditor, edit: TextEditorEdit, ...args): any; +} + +export class ShowBlameCommand extends EditorCommand { constructor(private git: GitProvider, private blameController: GitBlameController) { super(Commands.ShowBlame); } - execute(uri?: Uri, range?: Range, sha?: string) { - const editor = window.activeTextEditor; - if (!editor) return; - - if (!range) { - range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000)); + execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) { + if (sha) { + return this.blameController.toggleBlame(editor, sha); } + const activeLine = editor.selection.active.line; + return this.git.getBlameForLine(editor.document.fileName, activeLine) + .then(blame => this.blameController.showBlame(editor, blame.commit.sha)); + } +} + +export class ToggleBlameCommand extends EditorCommand { + constructor(private git: GitProvider, private blameController: GitBlameController) { + super(Commands.ToggleBlame); + } + + execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) { if (sha) { return this.blameController.toggleBlame(editor, sha); } @@ -44,15 +68,15 @@ export class BlameCommand extends Command { } } -export class HistoryCommand extends Command { +export class ShowHistoryCommand extends EditorCommand { constructor(private git: GitProvider) { super(Commands.ShowHistory); } - execute(uri?: Uri, range?: Range, position?: Position) { + execute(editor: TextEditor, edit: TextEditorEdit, 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; + const doc = editor.document; if (doc) { uri = doc.uri; range = doc.validateRange(new Range(0, 0, 1000000, 1000000)); @@ -68,28 +92,41 @@ export class HistoryCommand extends Command { } } -export class DiffWithPreviousCommand extends Command { +export class DiffWithPreviousCommand extends EditorCommand { 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 => { + execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string, compareWithSha?: string) { + if (!sha) { + return this.git.getBlameForLine(uri.path, editor.selection.active.line) + .then(blame => commands.executeCommand(Commands.DiffWithPrevious, uri, blame.commit.sha, blame.commit.previousSha)); + } + + if (!compareWithSha) { + return window.showInformationMessage(`Commit ${sha} has no previous commit`); + } + + return Promise.all([this.git.getVersionedFile(uri.path, sha), this.git.getVersionedFile(uri.path, compareWithSha)]) + .then(values => { + const [source, compare] = values; const fileName = basename(uri.path); return commands.executeCommand(VsCodeCommands.Diff, Uri.file(compare), Uri.file(source), `${fileName} (${compareWithSha}) ↔ ${fileName} (${sha})`); - }) - }); + }); } } -export class DiffWithWorkingCommand extends Command { +export class DiffWithWorkingCommand extends EditorCommand { constructor(private git: GitProvider) { super(Commands.DiffWithWorking); } - execute(uri?: Uri, sha?: string) { + execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) { + if (!sha) { + return this.git.getBlameForLine(uri.path, editor.selection.active.line) + .then(blame => commands.executeCommand(Commands.DiffWithWorking, uri, blame.commit.sha)); + }; + return this.git.getVersionedFile(uri.path, sha).then(compare => { const fileName = basename(uri.path); return commands.executeCommand(VsCodeCommands.Diff, Uri.file(compare), uri, `${fileName} (${sha}) ↔ ${fileName} (index)`); diff --git a/src/constants.ts b/src/constants.ts index e1b4bdc..9d3012f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,19 +1,16 @@ 'use strict' -export type WorkspaceState = 'hasGitHistoryExtension' | 'repoPath'; -export const WorkspaceState = { - HasGitHistoryExtension: 'hasGitHistoryExtension' as WorkspaceState, - RepoPath: 'repoPath' as WorkspaceState -} +export const DiagnosticCollectionName = 'gitlens'; +export const DiagnosticSource = 'GitLens'; +export const RepoPath = 'repoPath'; -export const RepoPath: string = 'repoPath'; - -export type Commands = 'git.action.diffWithPrevious' | 'git.action.diffWithWorking' | 'git.action.showBlame' | 'git.action.showHistory'; +export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showHistory' | 'gitlens.toggleBlame'; export const Commands = { - DiffWithPrevious: 'git.action.diffWithPrevious' as Commands, - DiffWithWorking: 'git.action.diffWithWorking' as Commands, - ShowBlame: 'git.action.showBlame' as Commands, - ShowHistory: 'git.action.showHistory' as Commands, + DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, + DiffWithWorking: 'gitlens.diffWithWorking' as Commands, + ShowBlame: 'gitlens.showBlame' as Commands, + ShowHistory: 'gitlens.showHistory' as Commands, + ToggleBlame: 'gitlens.toggleBlame' as Commands, } export type DocumentSchemes = 'file' | 'git' | 'gitblame'; @@ -29,4 +26,10 @@ export const VsCodeCommands = { ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as VsCodeCommands, ExecuteCodeLensProvider: 'vscode.executeCodeLensProvider' as VsCodeCommands, ShowReferences: 'editor.action.showReferences' as VsCodeCommands +} + +export type WorkspaceState = 'hasGitHistoryExtension' | 'repoPath'; +export const WorkspaceState = { + HasGitHistoryExtension: 'hasGitHistoryExtension' as WorkspaceState, + RepoPath: 'repoPath' as WorkspaceState } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 0b70ca8..bfc164e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,12 +1,11 @@ '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, HistoryCommand} from './commands'; +import {DiffWithPreviousCommand, DiffWithWorkingCommand, ShowBlameCommand, ShowHistoryCommand, ToggleBlameCommand} from './commands'; import {WorkspaceState} from './constants'; // this method is called when your extension is activated @@ -30,14 +29,14 @@ export function activate(context: ExtensionContext) { 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))); const blameController = new GitBlameController(context, git); context.subscriptions.push(blameController); - context.subscriptions.push(new BlameCommand(git, blameController)); - context.subscriptions.push(new HistoryCommand(git)); + context.subscriptions.push(new ShowBlameCommand(git, blameController)); + context.subscriptions.push(new ToggleBlameCommand(git, blameController)); + context.subscriptions.push(new ShowHistoryCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git)); context.subscriptions.push(new DiffWithWorkingCommand(git)); }).catch(reason => console.warn(reason)); diff --git a/src/gitBlameController.ts b/src/gitBlameController.ts index 519d3ae..1a1b56d 100644 --- a/src/gitBlameController.ts +++ b/src/gitBlameController.ts @@ -1,53 +1,62 @@ 'use strict' -import {commands, DecorationOptions, Disposable, ExtensionContext, languages, OverviewRulerLane, Position, Range, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode'; +import {commands, DecorationOptions, Diagnostic, DiagnosticCollection, DiagnosticSeverity, 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 GitCodeActionsProvider from './gitCodeActionProvider'; +import {DiagnosticCollectionName, DiagnosticSource} from './constants'; import * as moment from 'moment'; +const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ + before: { + color: '#5a5a5a', + margin: '0 1em 0 0', + width: '5em' + }, +}); + +let highlightDecoration: TextEditorDecorationType; + export default class GitBlameController extends Disposable { private _controller: GitBlameEditorController; - private _subscription: Disposable; + private _disposable: Disposable; private _blameDecoration: TextEditorDecorationType; private _highlightDecoration: TextEditorDecorationType; - constructor(context: ExtensionContext, private git: GitProvider) { + constructor(private context: ExtensionContext, private git: GitProvider) { super(() => this.dispose()); - this._blameDecoration = window.createTextEditorDecorationType({ - before: { - color: '#5a5a5a', - margin: '0 1em 0 0', - width: '5em' + if (!highlightDecoration) { + highlightDecoration = 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._highlightDecoration= 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 - }); + const subscriptions: Disposable[] = []; - this._subscription = Disposable.from(window.onDidChangeActiveTextEditor(e => { + subscriptions.push(window.onDidChangeActiveTextEditor(e => { if (!this._controller || this._controller.editor === e) return; this.clear(); })); + + this._disposable = Disposable.from(...subscriptions); } dispose() { this.clear(); - this._subscription && this._subscription.dispose(); + this._disposable && this._disposable.dispose(); } clear() { @@ -55,78 +64,106 @@ export default class GitBlameController extends Disposable { this._controller = null; } + showBlame(editor: TextEditor, sha: string) { + if (!editor) { + this.clear(); + return; + } + + if (!this._controller) { + this._controller = new GitBlameEditorController(this.context, this.git, editor); + return this._controller.applyBlame(sha); + } + } + toggleBlame(editor: TextEditor, sha: string) { - if (editor && (this._controller && this._controller.sha !== sha)) { - this._controller.applyHighlight(sha); + if (!editor || this._controller) { + this.clear(); return; } - const controller = this._controller; - this.clear(); - - if (!editor || (controller && controller.sha === sha)) { - return; - } - - this._controller = new GitBlameEditorController(this.git, this._blameDecoration, this._highlightDecoration, editor, sha); - return this._controller.applyBlame(sha); + return this.showBlame(editor, sha); } } class GitBlameEditorController extends Disposable { - private _subscription: Disposable; + private _disposable: Disposable; private _blame: Promise; - //private _commits: Promise>; + private _diagnostics: DiagnosticCollection; - constructor(private git: GitProvider, private blameDecoration: TextEditorDecorationType, private highlightDecoration: TextEditorDecorationType, public editor: TextEditor, public sha: string) { + constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) { super(() => this.dispose()); const fileName = this.editor.document.uri.path; this._blame = this.git.getBlameForFile(fileName); - //this._commits = this.git.getCommitMessages(fileName); - this._subscription = Disposable.from(window.onDidChangeTextEditorSelection(e => { + const subscriptions: Disposable[] = []; + + this._diagnostics = languages.createDiagnosticCollection(DiagnosticCollectionName); + subscriptions.push(this._diagnostics); + + subscriptions.push(languages.registerCodeActionsProvider(GitCodeActionsProvider.selector, new GitCodeActionsProvider(this.context, this.git))); + + subscriptions.push(window.onDidChangeTextEditorSelection(e => { const activeLine = e.selections[0].active.line; + + this._diagnostics.clear(); + this.git.getBlameForLine(e.textEditor.document.fileName, activeLine) - .then(blame => this.applyHighlight(blame.commit.sha)); + .then(blame => { + // Add the bogus diagnostics to provide code actions for this sha + this._diagnostics.set(editor.document.uri, [this._getDiagnostic(editor, activeLine, blame.commit.sha)]); + + this.applyHighlight(blame.commit.sha); + }); })); + + this._disposable = Disposable.from(...subscriptions); } dispose() { if (this.editor) { - this.editor.setDecorations(this.blameDecoration, []); - this.editor.setDecorations(this.highlightDecoration, []); + this.editor.setDecorations(blameDecoration, []); + this.editor.setDecorations(highlightDecoration, []); this.editor = null; } - this._subscription && this._subscription.dispose(); + this._disposable && this._disposable.dispose(); + } + + _getDiagnostic(editor, line, sha) { + const diag = new Diagnostic(editor.document.validateRange(new Range(line, 0, line, 1000000)), `Diff commit ${sha}`, DiagnosticSeverity.Hint); + diag.source = DiagnosticSource; + return diag; } applyBlame(sha: string) { return this._blame.then(blame => { if (!blame.lines.length) return; - // return this._commits.then(msgs => { - // const commits = Array.from(blame.commits.values()); - // commits.forEach(c => c.message = msgs.get(c.sha.substring(0, c.sha.length - 1))); + const blameDecorationOptions: DecorationOptions[] = blame.lines.map(l => { + const c = blame.commits.get(l.sha); + return { + range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), + hoverMessage: `${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, + renderOptions: { before: { contentText: `${l.sha.substring(0, 8)}`, } } + }; + }); - const blameDecorationOptions: DecorationOptions[] = blame.lines.map(l => { - const c = blame.commits.get(l.sha); - return { - range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)), - hoverMessage: `${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`, - renderOptions: { before: { contentText: `${l.sha.substring(0, 8)}`, } } - }; - }); + this.editor.setDecorations(blameDecoration, blameDecorationOptions); - this.editor.setDecorations(this.blameDecoration, blameDecorationOptions); - return this.applyHighlight(sha || blame.commits.values().next().value.sha); - // }); + sha = sha || blame.commits.values().next().value.sha; + + // Add the bogus diagnostics to provide code actions for this sha + const activeLine = this.editor.selection.active.line; + this._diagnostics.clear(); + this._diagnostics.set(this.editor.document.uri, [this._getDiagnostic(this.editor, activeLine, sha)]); + + return this.applyHighlight(sha); }); } applyHighlight(sha: string) { - this.sha = sha; return this._blame.then(blame => { if (!blame.lines.length) return; @@ -134,7 +171,7 @@ class GitBlameEditorController extends Disposable { .filter(l => l.sha === sha) .map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); - this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); + this.editor.setDecorations(highlightDecoration, highlightDecorationRanges); }); } } \ No newline at end of file diff --git a/src/gitCodeActionProvider.ts b/src/gitCodeActionProvider.ts new file mode 100644 index 0000000..9415c02 --- /dev/null +++ b/src/gitCodeActionProvider.ts @@ -0,0 +1,39 @@ +'use strict'; +import {CancellationToken, CodeActionContext, CodeActionProvider, Command, DocumentSelector, ExtensionContext, Range, TextDocument, Uri, window} from 'vscode'; +import {Commands, DocumentSchemes} from './constants'; +import GitProvider from './gitProvider'; +import {DiagnosticSource} from './constants'; + +export default class GitCodeActionProvider implements CodeActionProvider { + static selector: DocumentSelector = { scheme: DocumentSchemes.File }; + + constructor(context: ExtensionContext, private git: GitProvider) { } + + provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Command[] | Thenable { + if (!context.diagnostics.some(d => d.source !== DiagnosticSource)) { + return []; + } + + return this.git.getBlameForLine(document.fileName, range.start.line) + .then(blame => { + const actions: Command[] = []; + if (blame.commit.sha) { + actions.push({ + title: `GitLens: Diff ${blame.commit.sha} with working tree`, + command: Commands.DiffWithWorking, + arguments: [Uri.file(document.fileName), blame.commit.sha, range] + }); + } + + if (blame.commit.sha && blame.commit.previousSha) { + actions.push({ + title: `GitLens: Diff ${blame.commit.sha} with previous ${blame.commit.previousSha}`, + command: Commands.DiffWithPrevious, + arguments: [Uri.file(document.fileName), blame.commit.sha, blame.commit.previousSha, range] + }); + } + + return actions; + }); + } +} \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 958d97d..d4ee5f4 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,7 +1,7 @@ 'use strict'; -import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window} from 'vscode'; import {Commands, DocumentSchemes, VsCodeCommands, WorkspaceState} from './constants'; -import GitProvider, {IGitBlame, IGitCommit} from './gitProvider'; +import GitProvider, {IGitBlame, IGitBlameLines, IGitCommit} from './gitProvider'; import * as moment from 'moment'; export class GitRecentChangeCodeLens extends CodeLens { @@ -9,36 +9,34 @@ export class GitRecentChangeCodeLens extends CodeLens { super(range); } - getBlame(): Promise { + 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) { super(range); } - getBlame(): Promise { + 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 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 }; - private hasGitHistoryExtension: boolean; + // private hasGitHistoryExtension: boolean; constructor(context: ExtensionContext, private git: GitProvider) { - this.hasGitHistoryExtension = context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false); + // this.hasGitHistoryExtension = context.workspaceState.get(WorkspaceState.HasGitHistoryExtension, false); } provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { @@ -116,7 +114,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { 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); + // if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token); } _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable { @@ -133,23 +131,26 @@ export default class GitCodeLensProvider implements CodeLensProvider { _resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable { return lens.getBlame().then(blame => { + const editor = window.activeTextEditor; + const activeLine = editor.selection.active.line; + 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] + command: Commands.ToggleBlame, + arguments: [Uri.file(lens.fileName), blame.allLines[activeLine].sha] }; return lens; }); } - _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); - } + // _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 03b832c..b7d671b 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -1,6 +1,7 @@ 'use strict' -import {Disposable, ExtensionContext, Location, Position, Range, Uri, workspace} from 'vscode'; +import {Disposable, ExtensionContext, languages, Location, Position, Range, Uri, workspace} from 'vscode'; import {DocumentSchemes, WorkspaceState} from './constants'; +import GitCodeLensProvider from './gitCodeLensProvider'; import Git from './git'; import {basename, dirname, extname} from 'path'; import * as moment from 'moment'; @@ -15,30 +16,52 @@ export default class GitProvider extends Disposable { public repoPath: string; private _blames: Map>; - private _subscription: Disposable; + private _disposable: Disposable; + private _codeLensProviderSubscription: Disposable; - constructor(context: ExtensionContext) { + // TODO: Needs to be a Map so it can debounce per file + private _clearCacheFn: ((string, boolean) => void) & _.Cancelable; + + constructor(private context: ExtensionContext) { super(() => this.dispose()); this.repoPath = context.workspaceState.get(WorkspaceState.RepoPath) as string; this._blames = new Map(); - 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)) - ); + this._registerCodeLensProvider(); + this._clearCacheFn = _.debounce(this._clearBlame.bind(this), 2500); + + const subscriptions: Disposable[] = []; + + subscriptions.push(workspace.onDidCloseTextDocument(d => this._clearBlame(d.fileName))); + subscriptions.push(workspace.onDidSaveTextDocument(d => this._clearCacheFn(d.fileName, true))); + subscriptions.push(workspace.onDidChangeTextDocument(e => this._clearCacheFn(e.document.fileName, false))); + + this._disposable = Disposable.from(...subscriptions); } dispose() { this._blames.clear(); - this._subscription && this._subscription.dispose(); + this._disposable && this._disposable.dispose(); + this._codeLensProviderSubscription && this._codeLensProviderSubscription.dispose(); } - private _removeFile(fileName: string) { + private _registerCodeLensProvider() { + if (this._codeLensProviderSubscription) { + this._codeLensProviderSubscription.dispose(); + } + this._codeLensProviderSubscription = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this)); + } + + private _clearBlame(fileName: string, reset?: boolean) { fileName = Git.normalizePath(fileName, this.repoPath); - this._blames.delete(fileName); + this._blames.delete(fileName.toLowerCase()); + + console.log(`_removeFile(${fileName}, ${reset})`); + + if (reset) { + this._registerCodeLensProvider(); + } } getRepoPath(cwd: string) { @@ -48,7 +71,7 @@ export default class GitProvider extends Disposable { // getBlameForFile(fileName: string) { // fileName = Git.normalizePath(fileName, this.repoPath); - // let blame = this._blames.get(fileName); + // let blame = this._blames.get(fileName.toLowerCase()); // if (blame !== undefined) return blame; // blame = Git.blame(fileName, this.repoPath) @@ -118,14 +141,14 @@ export default class GitProvider extends Disposable { // }; // }); - // this._blames.set(fileName, blame); + // this._blames.set(fileName.toLowerCase(), blame); // return blame; // } getBlameForFile(fileName: string) { fileName = Git.normalizePath(fileName, this.repoPath); - let blame = this._blames.get(fileName); + let blame = this._blames.get(fileName.toLowerCase()); if (blame !== undefined) return blame; blame = Git.blamePorcelain(fileName, this.repoPath) @@ -190,7 +213,7 @@ export default class GitProvider extends Disposable { .sort((a, b) => b.lineCount - a.lineCount) .forEach(a => sortedAuthors.set(a.name, a)); - const sortedCommits = new Map(); + const sortedCommits: Map = new Map(); Array.from(commits.values()) .sort((a, b) => b.date.getTime() - a.date.getTime()) .forEach(c => sortedCommits.set(c.sha, c)); @@ -202,8 +225,9 @@ export default class GitProvider extends Disposable { }; }); - this._blames.set(fileName, blame); - return blame; } + this._blames.set(fileName.toLowerCase(), blame); + return blame; + } getBlameForLine(fileName: string, line: number): Promise { return this.getBlameForFile(fileName).then(blame => { @@ -217,12 +241,12 @@ export default class GitProvider extends Disposable { }); } - getBlameForRange(fileName: string, range: Range): Promise { + getBlameForRange(fileName: string, range: Range): Promise { return this.getBlameForFile(fileName).then(blame => { - if (!blame.lines.length) return blame; + if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { - return blame; + return Object.assign({ allLines: blame.lines }, blame); } const lines = blame.lines.slice(range.start.line, range.end.line + 1); @@ -249,16 +273,21 @@ export default class GitProvider extends Disposable { author.lineCount += commit.lines.length; }); - const sortedAuthors = new Map(); + const sortedAuthors: Map = 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 }; + return { + authors: sortedAuthors, + commits: commits, + lines: lines, + allLines: blame.lines + }; }); } - getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { + 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 }); @@ -391,7 +420,11 @@ export interface IGitBlameLine { line: IGitCommitLine; } -export interface IGitBlameLines { +export interface IGitBlameLines extends IGitBlame { + allLines: IGitCommitLine[]; +} + +export interface IGitBlameCommitLines { author: IGitAuthor; commit: IGitCommit; lines: IGitCommitLine[];