diff --git a/README.md b/README.md index 9b707e8..9bdb79c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 9d84b42..a1d7279 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 8a762d1..50207cb 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -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; - 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('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('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 = [`_${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 { - 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 = [`_${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 { - 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; - } - } } \ No newline at end of file diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts new file mode 100644 index 0000000..f72b67c --- /dev/null +++ b/src/blameAnnotationProvider.ts @@ -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; + 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('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('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 = [`_${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 { + 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 = [`_${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 { + 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; + } + } +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts deleted file mode 100644 index cbd810c..0000000 --- a/src/commands.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/commands/commands.ts b/src/commands/commands.ts new file mode 100644 index 0000000..d851f65 --- /dev/null +++ b/src/commands/commands.ts @@ -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; +} \ No newline at end of file diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts new file mode 100644 index 0000000..388b9e7 --- /dev/null +++ b/src/commands/diffWithPrevious.ts @@ -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'}))); + } +} \ No newline at end of file diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts new file mode 100644 index 0000000..4d1310d --- /dev/null +++ b/src/commands/diffWithWorking.ts @@ -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'}))); + } +} diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts new file mode 100644 index 0000000..c6fa749 --- /dev/null +++ b/src/commands/showBlame.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts new file mode 100644 index 0000000..5179120 --- /dev/null +++ b/src/commands/showBlameHistory.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts new file mode 100644 index 0000000..fa3af71 --- /dev/null +++ b/src/commands/toggleBlame.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/commands/toggleCodeLens.ts b/src/commands/toggleCodeLens.ts new file mode 100644 index 0000000..171c82c --- /dev/null +++ b/src/commands/toggleCodeLens.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index a6e3973..1d2fc81 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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) { diff --git a/src/git/git.ts b/src/git/git.ts index 8d1ba56..29edde2 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -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; }); diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts index a70f589..522c9fa 100644 --- a/src/gitBlameCodeLensProvider.ts +++ b/src/gitBlameCodeLensProvider.ts @@ -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); } diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index b4b70d9..5bf4f4e 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -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; }