Browse Source

Fixes another off-by-one issue when diffing with caching

Refactored commands and blame annotations
main
Eric Amodio 8 years ago
parent
commit
d2d72f0d54
16 changed files with 519 additions and 455 deletions
  1. +5
    -1
      README.md
  2. +4
    -4
      package.json
  3. +13
    -254
      src/blameAnnotationController.ts
  4. +246
    -0
      src/blameAnnotationProvider.ts
  5. +0
    -189
      src/commands.ts
  6. +33
    -0
      src/commands/commands.ts
  7. +51
    -0
      src/commands/diffWithPrevious.ts
  8. +40
    -0
      src/commands/diffWithWorking.ts
  9. +27
    -0
      src/commands/showBlame.ts
  10. +26
    -0
      src/commands/showBlameHistory.ts
  11. +37
    -0
      src/commands/toggleBlame.ts
  12. +15
    -0
      src/commands/toggleCodeLens.ts
  13. +6
    -1
      src/extension.ts
  14. +3
    -3
      src/git/git.ts
  15. +4
    -2
      src/gitBlameCodeLensProvider.ts
  16. +9
    -1
      src/gitCodeLensProvider.ts

+ 5
- 1
README.md View File

@ -56,6 +56,10 @@ Must be using Git and it must be in your path.
---
## Release Notes
### 0.5.5
- Fixes another off-by-one issue when diffing with caching
### 0.5.4
- Fixes off-by-one issues with blame annotations without caching and when diffing with a previous version
@ -71,7 +75,7 @@ Must be using Git and it must be in your path.
### 0.5.1
- Adds blame information in the statusBar
- Adds blame information in the StatusBar
- Add new StatusBar settings -- see **Extension Settings** above for details
- Renames the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings options (to align with command names)
- Adds new `gitlens.diffWithPrevious` option to the `gitlens.codeLens.recentChange.command` & `gitlens.codeLens.authors.command` settings

+ 4
- 4
package.json View File

@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "0.5.4",
"version": "0.5.5",
"author": {
"name": "Eric Amodio",
"email": "eamodio@gmail.com"
@ -217,13 +217,13 @@
"lodash.debounce": "^4.0.8",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.4.0",
"lodash": "^4.16.0",
"moment": "^2.15.0",
"lodash": "^4.16.1",
"moment": "^2.15.1",
"spawn-rx": "^2.0.1",
"tmp": "^0.0.29"
},
"devDependencies": {
"typescript": "^2.0.2",
"typescript": "^2.0.3",
"vscode": "^0.11.17"
},
"scripts": {

+ 13
- 254
src/blameAnnotationController.ts View File

@ -1,42 +1,15 @@
'use strict'
import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace} from 'vscode';
import {BuiltInCommands, Commands, DocumentSchemes} from './constants';
import {BlameAnnotationStyle, IBlameConfig} from './configuration';
import GitProvider, {IGitBlame, IGitCommit} from './gitProvider';
import * as moment from 'moment';
const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
before: {
margin: '0 1.75em 0 0'
}
});
let highlightDecoration: TextEditorDecorationType;
import {Disposable, ExtensionContext, TextEditor, workspace} from 'vscode';
import {BlameAnnotationProvider} from './blameAnnotationProvider';
import GitProvider from './gitProvider';
export default class BlameAnnotationController extends Disposable {
private _disposable: Disposable;
private _editorController: EditorBlameAnnotationController|null;
private _annotationProvider: BlameAnnotationProvider|null;
constructor(private context: ExtensionContext, private git: GitProvider) {
super(() => this.dispose());
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
});
}
const subscriptions: Disposable[] = [];
// subscriptions.push(window.onDidChangeActiveTextEditor(e => {
@ -44,10 +17,10 @@ export default class BlameAnnotationController extends Disposable {
// this.clear();
// }));
workspace.onDidCloseTextDocument(d => {
if (!this._editorController || this._editorController.uri.toString() !== d.uri.toString()) return;
subscriptions.push(workspace.onDidCloseTextDocument(d => {
if (!this._annotationProvider || this._annotationProvider.uri.toString() !== d.uri.toString()) return;
this.clear();
})
}));
this._disposable = Disposable.from(...subscriptions);
}
@ -58,8 +31,8 @@ export default class BlameAnnotationController extends Disposable {
}
clear() {
this._editorController && this._editorController.dispose();
this._editorController = null;
this._annotationProvider && this._annotationProvider.dispose();
this._annotationProvider = null;
}
showBlameAnnotation(editor: TextEditor, sha?: string) {
@ -68,232 +41,18 @@ export default class BlameAnnotationController extends Disposable {
return;
}
if (!this._editorController) {
this._editorController = new EditorBlameAnnotationController(this.context, this.git, editor);
return this._editorController.applyBlameAnnotation(sha);
if (!this._annotationProvider) {
this._annotationProvider = new BlameAnnotationProvider(this.context, this.git, editor);
return this._annotationProvider.provideBlameAnnotation(sha);
}
}
toggleBlameAnnotation(editor: TextEditor, sha?: string) {
if (!editor ||!editor.document || editor.document.isUntitled || this._editorController) {
if (!editor ||!editor.document || editor.document.isUntitled || this._annotationProvider) {
this.clear();
return;
}
return this.showBlameAnnotation(editor, sha);
}
}
class EditorBlameAnnotationController extends Disposable {
public uri: Uri;
private _blame: Promise<IGitBlame>;
private _config: IBlameConfig;
private _disposable: Disposable;
private _document: TextDocument;
private _toggleWhitespace: boolean;
constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) {
super(() => this.dispose());
this._document = this.editor.document;
this.uri = this._document.uri;
this._blame = this.git.getBlameForFile(this.uri.fsPath);
this._config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this));
this._disposable = Disposable.from(...subscriptions);
}
dispose() {
if (this.editor) {
// HACK: This only works when switching to another editor - diffs handle whitespace toggle differently
if (this._toggleWhitespace) {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
this.editor.setDecorations(blameDecoration, []);
this.editor.setDecorations(highlightDecoration, []);
}
this._disposable && this._disposable.dispose();
}
private _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line)
.then(blame => blame && this.applyHighlight(blame.commit.sha));
}
applyBlameAnnotation(sha?: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
const whitespace = workspace.getConfiguration('editor').get<string>('renderWhitespace');
this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none';
if (this._toggleWhitespace) {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
let blameDecorationOptions: DecorationOptions[] | undefined;
switch (this._config.annotation.style) {
case BlameAnnotationStyle.Compact:
blameDecorationOptions = this._getCompactGutterDecorations(blame);
break;
case BlameAnnotationStyle.Expanded:
blameDecorationOptions = this._getExpandedGutterDecorations(blame);
break;
}
if (blameDecorationOptions) {
this.editor.setDecorations(blameDecoration, blameDecorationOptions);
}
sha = sha || blame.commits.values().next().value.sha;
return this.applyHighlight(sha);
});
}
applyHighlight(sha: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
});
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let count = 0;
let lastSha;
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
}
let gutter = '';
if (lastSha !== l.sha) {
count = -1;
}
const isEmptyOrWhitespace = this._document.lineAt(l.line).isEmptyOrWhitespace;
if (!isEmptyOrWhitespace) {
switch (++count) {
case 0:
gutter = commit.sha.substring(0, 8);
break;
case 1:
gutter = `\\00a6\\00a0 ${this._getAuthor(commit, 17, true)}`;
break;
case 2:
gutter = `\\00a6\\00a0 ${this._getDate(commit, true)}`;
break;
default:
gutter = '\\00a6\\00a0';
break;
}
}
lastSha = l.sha;
return <DecorationOptions>{
range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
hoverMessage: hoverMessage,
renderOptions: { before: { color: color, contentText: gutter, width: '11em' } }
};
});
}
private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let width = 0;
if (this._config.annotation.sha) {
width += 5;
}
if (this._config.annotation.date) {
if (width > 0) {
width += 7;
} else {
width += 6;
}
}
if (this._config.annotation.author) {
if (width > 5 + 6) {
width += 12;
} else if (width > 0) {
width += 11;
} else {
width += 10;
}
}
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
}
const gutter = this._getGutter(commit);
return <DecorationOptions>{
range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
hoverMessage: hoverMessage,
renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } }
};
});
}
private _getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) {
if (!force && !this._config.annotation.author) return '';
let author = commit.isUncommitted ? 'Uncommitted': commit.author;
if (author.length > max) {
return `${author.substring(0, max - 1)}\\2026`;
}
return author;
}
private _getDate(commit: IGitCommit, force?: boolean) {
if (!force && !this._config.annotation.date) return '';
return moment(commit.date).format('MM/DD/YYYY');
}
private _getGutter(commit: IGitCommit) {
const author = this._getAuthor(commit);
const date = this._getDate(commit);
if (this._config.annotation.sha) {
return `${commit.sha.substring(0, 8)}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`;
} else if (this._config.annotation.date) {
return `${date}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`;
} else {
return author;
}
}
}

+ 246
- 0
src/blameAnnotationProvider.ts View File

@ -0,0 +1,246 @@
'use strict'
import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace} from 'vscode';
import {BuiltInCommands} from './constants';
import {BlameAnnotationStyle, IBlameConfig} from './configuration';
import GitProvider, {IGitBlame, IGitCommit} from './gitProvider';
import * as moment from 'moment';
const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
before: {
margin: '0 1.75em 0 0'
}
});
let highlightDecoration: TextEditorDecorationType;
export class BlameAnnotationProvider extends Disposable {
public uri: Uri;
private _blame: Promise<IGitBlame>;
private _config: IBlameConfig;
private _disposable: Disposable;
private _document: TextDocument;
private _toggleWhitespace: boolean;
constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) {
super(() => this.dispose());
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._document = this.editor.document;
this.uri = this._document.uri;
this._blame = this.git.getBlameForFile(this.uri.fsPath);
this._config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this));
this._disposable = Disposable.from(...subscriptions);
}
dispose() {
if (this.editor) {
// HACK: This only works when switching to another editor - diffs handle whitespace toggle differently
if (this._toggleWhitespace) {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
this.editor.setDecorations(blameDecoration, []);
this.editor.setDecorations(highlightDecoration, []);
}
this._disposable && this._disposable.dispose();
}
private _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line)
.then(blame => blame && this._applyCommitHighlight(blame.commit.sha));
}
provideBlameAnnotation(sha?: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
const whitespace = workspace.getConfiguration('editor').get<string>('renderWhitespace');
this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none';
if (this._toggleWhitespace) {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
let blameDecorationOptions: DecorationOptions[] | undefined;
switch (this._config.annotation.style) {
case BlameAnnotationStyle.Compact:
blameDecorationOptions = this._getCompactGutterDecorations(blame);
break;
case BlameAnnotationStyle.Expanded:
blameDecorationOptions = this._getExpandedGutterDecorations(blame);
break;
}
if (blameDecorationOptions) {
this.editor.setDecorations(blameDecoration, blameDecorationOptions);
}
sha = sha || blame.commits.values().next().value.sha;
return this._applyCommitHighlight(sha);
});
}
private _applyCommitHighlight(sha: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
});
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let count = 0;
let lastSha;
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
}
let gutter = '';
if (lastSha !== l.sha) {
count = -1;
}
const isEmptyOrWhitespace = this._document.lineAt(l.line).isEmptyOrWhitespace;
if (!isEmptyOrWhitespace) {
switch (++count) {
case 0:
gutter = commit.sha.substring(0, 8);
break;
case 1:
gutter = `\\00a6\\00a0 ${this._getAuthor(commit, 17, true)}`;
break;
case 2:
gutter = `\\00a6\\00a0 ${this._getDate(commit, true)}`;
break;
default:
gutter = '\\00a6\\00a0';
break;
}
}
lastSha = l.sha;
return <DecorationOptions>{
range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
hoverMessage: hoverMessage,
renderOptions: { before: { color: color, contentText: gutter, width: '11em' } }
};
});
}
private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let width = 0;
if (this._config.annotation.sha) {
width += 5;
}
if (this._config.annotation.date) {
if (width > 0) {
width += 7;
} else {
width += 6;
}
}
if (this._config.annotation.author) {
if (width > 5 + 6) {
width += 12;
} else if (width > 0) {
width += 11;
} else {
width += 10;
}
}
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
}
const gutter = this._getGutter(commit);
return <DecorationOptions>{
range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
hoverMessage: hoverMessage,
renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } }
};
});
}
private _getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) {
if (!force && !this._config.annotation.author) return '';
let author = commit.isUncommitted ? 'Uncommitted': commit.author;
if (author.length > max) {
return `${author.substring(0, max - 1)}\\2026`;
}
return author;
}
private _getDate(commit: IGitCommit, force?: boolean) {
if (!force && !this._config.annotation.date) return '';
return moment(commit.date).format('MM/DD/YYYY');
}
private _getGutter(commit: IGitCommit) {
const author = this._getAuthor(commit);
const date = this._getDate(commit);
if (this._config.annotation.sha) {
return `${commit.sha.substring(0, 8)}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`;
} else if (this._config.annotation.date) {
return `${date}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`;
} else {
return author;
}
}
}

+ 0
- 189
src/commands.ts View File

@ -1,189 +0,0 @@
'use strict'
import {commands, DecorationOptions, Disposable, OverviewRulerLane, Position, Range, TextEditor, TextEditorEdit, TextEditorDecorationType, Uri, window} from 'vscode';
import {BuiltInCommands, Commands} from './constants';
import GitProvider from './gitProvider';
import BlameAnnotationController from './blameAnnotationController';
import * as moment from 'moment';
import * as path from 'path';
abstract class Command extends Disposable {
private _subscriptions: Disposable;
constructor(command: Commands) {
super(() => this.dispose());
this._subscriptions = commands.registerCommand(command, this.execute, this);
}
dispose() {
this._subscriptions && this._subscriptions.dispose();
}
abstract execute(...args): any;
}
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 DiffWithPreviousCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.DiffWithPrevious);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, compareWithSha?: string, compareWithUri?: Uri, line?: number) {
line = line || editor.selection.active.line + 1;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
// If the line is uncommitted, find the previous commit
const commit = blame.commit;
if (commit.isUncommitted) {
return this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex))
.then(prevBlame => {
if (!prevBlame) return;
const prevCommit = prevBlame.commit;
return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine);
});
}
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line);
});
}
if (!compareWithSha) {
return window.showInformationMessage(`Commit ${sha} has no previous commit`);
}
return Promise.all([this.git.getVersionedFile(shaUri.fsPath, repoPath, sha), this.git.getVersionedFile(compareWithUri.fsPath, repoPath, compareWithSha)])
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex))
.then(values => commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(compareWithUri.fsPath)} (${compareWithSha}) ↔ ${path.basename(shaUri.fsPath)} (${sha})`)
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, {lineNumber: line, at: 'center'})));
}
}
export class DiffWithWorkingCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.DiffWithWorking);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) {
line = line || editor.selection.active.line + 1;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
const commit = blame.commit;
// If the line is uncommitted, find the previous commit
if (commit.isUncommitted) {
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line + 1);
}
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line)
});
};
return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex))
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)}`)
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, {lineNumber: line, at: 'center'})));
}
}
export class ShowBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.ShowBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.annotationController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha));
}
}
export class ShowBlameHistoryCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ShowBlameHistory);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000));
position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start;
}
return this.git.getBlameLocations(uri.fsPath, range)
.catch(ex => console.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex))
.then(locations => commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations));
}
}
export class ToggleBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private blameController: BlameAnnotationController) {
super(Commands.ToggleBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.blameController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.blameController.toggleBlameAnnotation(editor, blame && blame.commit.sha));
}
}
export class ToggleCodeLensCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ToggleCodeLens);
}
execute(editor: TextEditor, edit: TextEditorEdit) {
return this.git.toggleCodeLens(editor);
}
}

+ 33
- 0
src/commands/commands.ts View File

@ -0,0 +1,33 @@
'use strict'
import {commands, Disposable, TextEditor, TextEditorEdit} from 'vscode';
import {Commands} from '../constants';
export abstract class Command extends Disposable {
private _subscriptions: Disposable;
constructor(command: Commands) {
super(() => this.dispose());
this._subscriptions = commands.registerCommand(command, this.execute, this);
}
dispose() {
this._subscriptions && this._subscriptions.dispose();
}
abstract execute(...args): any;
}
export 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;
}

+ 51
- 0
src/commands/diffWithPrevious.ts View File

@ -0,0 +1,51 @@
'use strict'
import {commands, TextEditor, TextEditorEdit, Uri, window} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
import GitProvider from '../gitProvider';
import * as path from 'path';
export default class DiffWithPreviousCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.DiffWithPrevious);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, compareWithSha?: string, compareWithUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
// If the line is uncommitted, find the previous commit
const commit = blame.commit;
if (commit.isUncommitted) {
return this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath)
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex))
.then(prevBlame => {
if (!prevBlame) return;
const prevCommit = prevBlame.commit;
return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine);
});
}
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line);
});
}
if (!compareWithSha) {
return window.showInformationMessage(`Commit ${sha} has no previous commit`);
}
return Promise.all([this.git.getVersionedFile(shaUri.fsPath, repoPath, sha), this.git.getVersionedFile(compareWithUri.fsPath, repoPath, compareWithSha)])
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex))
.then(values => commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(compareWithUri.fsPath)} (${compareWithSha}) ↔ ${path.basename(shaUri.fsPath)} (${sha})`)
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, {lineNumber: line, at: 'center'})));
}
}

+ 40
- 0
src/commands/diffWithWorking.ts View File

@ -0,0 +1,40 @@
'use strict'
import {commands, TextEditor, TextEditorEdit, Uri, window} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
import GitProvider from '../gitProvider';
import * as path from 'path';
export default class DiffWithWorkingCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.DiffWithWorking);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex))
.then(blame => {
if (!blame) return;
const commit = blame.commit;
// If the line is uncommitted, find the previous commit
if (commit.isUncommitted) {
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line + 1);
}
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line)
});
};
return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha)
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex))
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)}`)
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, {lineNumber: line, at: 'center'})));
}
}

+ 27
- 0
src/commands/showBlame.ts View File

@ -0,0 +1,27 @@
'use strict'
import {TextEditor, TextEditorEdit, Uri} from 'vscode';
import BlameAnnotationController from '../blameAnnotationController';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
import GitProvider from '../gitProvider';
export default class ShowBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.ShowBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.annotationController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha));
}
}

+ 26
- 0
src/commands/showBlameHistory.ts View File

@ -0,0 +1,26 @@
'use strict'
import {commands, Position, Range, TextEditor, TextEditorEdit, Uri} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
import GitProvider from '../gitProvider';
export default class ShowBlameHistoryCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ShowBlameHistory);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000));
position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start;
}
return this.git.getBlameLocations(uri.fsPath, range)
.catch(ex => console.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex))
.then(locations => commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations));
}
}

+ 37
- 0
src/commands/toggleBlame.ts View File

@ -0,0 +1,37 @@
'use strict'
import {TextEditor, TextEditorEdit, Uri} from 'vscode';
import BlameAnnotationController from '../blameAnnotationController';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
import GitProvider from '../gitProvider';
export default class ToggleBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private blameController: BlameAnnotationController) {
super(Commands.ToggleBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.blameController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.catch(ex => console.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex))
.then(blame => this.blameController.toggleBlameAnnotation(editor, blame && blame.commit.sha));
}
}
export class ToggleCodeLensCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ToggleCodeLens);
}
execute(editor: TextEditor, edit: TextEditorEdit) {
return this.git.toggleCodeLens(editor);
}
}

+ 15
- 0
src/commands/toggleCodeLens.ts View File

@ -0,0 +1,15 @@
'use strict'
import {TextEditor, TextEditorEdit} from 'vscode';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
import GitProvider from '../gitProvider';
export default class ToggleCodeLensCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ToggleCodeLens);
}
execute(editor: TextEditor, edit: TextEditorEdit) {
return this.git.toggleCodeLens(editor);
}
}

+ 6
- 1
src/extension.ts View File

@ -6,9 +6,14 @@ import GitContentProvider from './gitContentProvider';
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
import GitBlameContentProvider from './gitBlameContentProvider';
import GitProvider, {Git} from './gitProvider';
import {DiffWithPreviousCommand, DiffWithWorkingCommand, ShowBlameCommand, ShowBlameHistoryCommand, ToggleBlameCommand, ToggleCodeLensCommand} from './commands';
import {IStatusBarConfig} from './configuration';
import {WorkspaceState} from './constants';
import DiffWithPreviousCommand from './commands/diffWithPrevious';
import DiffWithWorkingCommand from './commands/diffWithWorking';
import ShowBlameCommand from './commands/showBlame';
import ShowBlameHistoryCommand from './commands/showBlameHistory';
import ToggleBlameCommand from './commands/toggleBlame';
import ToggleCodeLensCommand from './commands/toggleCodeLens';
// this method is called when your extension is activated
export function activate(context: ExtensionContext) {

+ 3
- 3
src/git/git.ts View File

@ -12,15 +12,15 @@ const UncommittedRegex = /^[0]+$/;
function gitCommand(cwd: string, ...args) {
return spawnPromise('git', args, { cwd: cwd })
.then(s => {
console.log('[GitLens]', 'git', ...args);
console.log('[GitLens]', 'git', ...args, cwd);
return s;
})
.catch(ex => {
const msg = ex && ex.toString();
if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) {
console.warn('[GitLens]', 'git', ...args, msg && msg.replace(/\r?\n|\r/g, ' '));
console.warn('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
} else {
console.error('[GitLens]', 'git', ...args, msg && msg.replace(/\r?\n|\r/g, ' '));
console.error('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
}
throw ex;
});

+ 4
- 2
src/gitBlameCodeLensProvider.ts View File

@ -72,7 +72,8 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider {
Uri.file(lens.fileName),
lens.commit.sha,
lens.commit.uri,
lens.range.start.line]
lens.range.start.line
]
};
return Promise.resolve(lens);
}
@ -88,7 +89,8 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider {
lens.commit.uri,
lens.commit.previousSha,
lens.commit.previousUri,
lens.range.start.line]
lens.range.start.line
]
};
return Promise.resolve(lens);
}

+ 9
- 1
src/gitCodeLensProvider.ts View File

@ -204,7 +204,15 @@ export default class GitCodeLensProvider implements CodeLensProvider {
lens.command = {
title: title,
command: Commands.DiffWithPrevious,
arguments: [Uri.file(lens.fileName), commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line.line]
arguments: [
Uri.file(lens.fileName),
commit.repoPath,
commit.sha,
commit.uri,
commit.previousSha,
commit.previousUri,
line.line
]
};
return lens;
}

Loading…
Cancel
Save