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/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/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/contentProvider.ts b/src/gitBlameContentProvider.ts similarity index 100% rename from src/contentProvider.ts rename to src/gitBlameContentProvider.ts diff --git a/src/codeLensProvider.ts b/src/gitCodeLensProvider.ts similarity index 71% rename from src/codeLensProvider.ts rename to src/gitCodeLensProvider.ts index 7a8bd27..6e5e15d 100644 --- a/src/codeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,10 +1,10 @@ 'use strict'; -import {CancellationToken, CodeLens, CodeLensProvider, commands, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; -import {Commands, VsCodeCommands, WorkspaceState} from './constants'; +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 GitBlameCodeLens extends CodeLens { +export class GitCodeLens extends CodeLens { constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) { super(range); } @@ -21,26 +21,30 @@ export class GitHistoryCodeLens extends CodeLens { } 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 { - this.git.getBlameForFile(document.fileName); + const fileName = document.fileName; + + this.git.getBlameForFile(fileName); return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise).then(symbols => { - let lenses: CodeLens[] = []; - symbols.forEach(sym => this._provideCodeLens(document, sym, lenses)); + 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 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))); + 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(document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void { + private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void { switch (symbol.kind) { case SymbolKind.Package: case SymbolKind.Module: @@ -66,16 +70,16 @@ export default class GitCodeLensProvider implements CodeLensProvider { 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)))); + 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 GitBlameCodeLens) return this._resolveGitBlameCodeLens(lens, token); + if (lens instanceof GitCodeLens) return this._resolveGitBlameCodeLens(lens, token); if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token); } - _resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable { + _resolveGitBlameCodeLens(lens: GitCodeLens, token: CancellationToken): Thenable { return new Promise((resolve, reject) => { lens.getBlame().then(blame => { if (!blame.lines.length) { @@ -104,4 +108,4 @@ export default class GitCodeLensProvider implements CodeLensProvider { }; 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;