Browse Source

Adds CodeLens for Diff'ing in blame

Other fixes and refactoring
main
Eric Amodio 8 years ago
parent
commit
70cc92ddd6
11 changed files with 364 additions and 231 deletions
  1. +1
    -1
      package.json
  2. +30
    -0
      src/commands.ts
  3. +7
    -3
      src/constants.ts
  4. +9
    -8
      src/extension.ts
  5. +6
    -1
      src/git.ts
  6. +87
    -0
      src/gitBlameCodeLensProvider.ts
  7. +0
    -0
      src/gitBlameContentProvider.ts
  8. +19
    -15
      src/gitCodeLensProvider.ts
  9. +3
    -1
      src/gitProvider.ts

+ 1
- 1
package.json View File

@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "0.0.2",
"version": "0.0.3",
"author": "Eric Amodio",
"publisher": "eamodio",
"engines": {

+ 30
- 0
src/commands.ts View File

@ -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})`);
});
}
}

+ 7
- 3
src/constants.ts View File

@ -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

+ 9
- 8
src/extension.ts View File

@ -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));
}

+ 6
- 1
src/git.ts View File

@ -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; })

+ 87
- 0
src/gitBlameCodeLensProvider.ts View File

@ -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<CodeLens[]> {
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<CodeLens> {
if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token);
if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token);
}
_resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable<CodeLens> {
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<CodeLens> {
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);
}
}

src/contentProvider.ts → src/gitBlameContentProvider.ts View File


src/codeLensProvider.ts → src/gitCodeLensProvider.ts View File

@ -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<CodeLens[]> {
this.git.getBlameForFile(document.fileName);
const fileName = document.fileName;
this.git.getBlameForFile(fileName);
return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<SymbolInformation[]>).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<CodeLens> {
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<CodeLens> {
_resolveGitBlameCodeLens(lens: GitCodeLens, token: CancellationToken): Thenable<CodeLens> {
return new Promise<CodeLens>((resolve, reject) => {
lens.getBlame().then(blame => {
if (!blame.lines.length) {
@ -104,4 +108,4 @@ export default class GitCodeLensProvider implements CodeLensProvider {
};
return Promise.resolve(lens);
}
}
}

+ 3
- 1
src/gitProvider.ts View File

@ -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;

Loading…
Cancel
Save