diff --git a/package.json b/package.json index 7ade48d..bd96569 100644 --- a/package.json +++ b/package.json @@ -784,6 +784,11 @@ "title": "Directory Compare", "category": "GitLens" }, + { + "command": "gitlens.diffWith", + "title": "Compare File Revisions", + "category": "GitLens" + }, { "command": "gitlens.diffWithBranch", "title": "Compare File with Branch...", @@ -1077,6 +1082,10 @@ "command": "gitlens.diffDirectory", "when": "gitlens:enabled" }, + { + "command": "gitlens.diffWith", + "when": "false" + }, { "command": "gitlens.diffWithBranch", "when": "gitlens:isTracked" diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 3ab73d6..00ae458 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -1,5 +1,6 @@ import { Strings } from '../system'; -import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode'; +import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode'; +import { DiffWithCommand, ShowQuickCommitDetailsCommand } from '../commands'; import { IThemeConfig, themeDefaults } from '../configuration'; import { GlyphChars } from '../constants'; import { CommitFormatter, GitCommit, GitDiffChunkLine, GitService, GitUri, ICommitFormatOptions } from '../gitService'; @@ -47,7 +48,7 @@ export class Annotations { return '#793738'; } - static getHoverMessage(commit: GitCommit, dateFormat: string | null): string | string[] { + static getHoverMessage(commit: GitCommit, dateFormat: string | null): MarkdownString { if (dateFormat === null) { dateFormat = 'MMMM Do, YYYY h:MMa'; } @@ -63,16 +64,21 @@ export class Annotations { .replace(/\n/g, ' \n'); message = `\n\n> ${message}`; } - return `\`${commit.shortSha}\`   __${commit.author}__, ${moment(commit.date).fromNow()}   _(${moment(commit.date).format(dateFormat)})_${message}`; + + const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})   __${commit.author}__, ${moment(commit.date).fromNow()}   _(${moment(commit.date).format(dateFormat)})_${message}`); + markdown.isTrusted = true; + return markdown; } - static getHoverDiffMessage(commit: GitCommit, chunkLine: GitDiffChunkLine | undefined): string | undefined { + static getHoverDiffMessage(commit: GitCommit, chunkLine: GitDiffChunkLine | undefined): MarkdownString | undefined { if (chunkLine === undefined) return undefined; const codeDiff = this._getCodeDiff(chunkLine); - return commit.isUncommitted - ? `\`Changes\`   ${GlyphChars.Dash}   _uncommitted_\n${codeDiff}` - : `\`Changes\`   ${GlyphChars.Dash}   \`${commit.previousShortSha}\` ${GlyphChars.ArrowLeftRight} \`${commit.shortSha}\`\n${codeDiff}`; + const markdown = new MarkdownString(commit.isUncommitted + ? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)})   ${GlyphChars.Dash}   _uncommitted_\n${codeDiff}` + : `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)})   ${GlyphChars.Dash}   [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)}) ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})\n${codeDiff}`); + markdown.isTrusted = true; + return markdown; } private static _getCodeDiff(chunkLine: GitDiffChunkLine): string { diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index 0d1475a..a69597a 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -1,5 +1,5 @@ 'use strict'; -import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; +import { DecorationOptions, ExtensionContext, MarkdownString, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; import { Annotations, endOfLineIndex } from './annotations'; import { FileAnnotationType } from './annotationController'; import { AnnotationProviderBase } from './annotationProvider'; @@ -48,7 +48,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { } as DecorationOptions); } - let message: string | undefined = undefined; + let message: MarkdownString | undefined = undefined; if (cfg.hover.changes) { message = Annotations.getHoverDiffMessage(commit, line); } diff --git a/src/commands.ts b/src/commands.ts index b00bde4..89427c3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -8,6 +8,7 @@ export * from './commands/copyShaToClipboard'; export * from './commands/diffDirectory'; export * from './commands/diffLineWithPrevious'; export * from './commands/diffLineWithWorking'; +export * from './commands/diffWith'; export * from './commands/diffWithBranch'; export * from './commands/diffWithNext'; export * from './commands/diffWithPrevious'; diff --git a/src/commands/common.ts b/src/commands/common.ts index d7a1e69..8d026f4 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -11,6 +11,7 @@ export type Commands = 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | 'gitlens.diffDirectory' | + 'gitlens.diffWith' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | @@ -52,6 +53,7 @@ export const Commands = { CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands, CopyShaToClipboard: 'gitlens.copyShaToClipboard' as Commands, DiffDirectory: 'gitlens.diffDirectory' as Commands, + DiffWith: 'gitlens.diffWith' as Commands, DiffWithBranch: 'gitlens.diffWithBranch' as Commands, DiffWithNext: 'gitlens.diffWithNext' as Commands, DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, @@ -166,6 +168,10 @@ function isTextEditor(editor: any): editor is TextEditor { export abstract class Command extends Disposable { + static getMarkdownCommandArgsCore(command: Commands, args: T): string { + return `command:${command}?${encodeURIComponent(JSON.stringify(args))}`; + } + protected readonly contextParsingOptions: CommandContextParsingOptions = { editor: false, uri: false }; private _disposable: Disposable; diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts new file mode 100644 index 0000000..cc847a2 --- /dev/null +++ b/src/commands/diffWith.ts @@ -0,0 +1,125 @@ +'use strict'; +import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands } from './common'; +import { BuiltInCommands, GlyphChars } from '../constants'; +import { GitCommit, GitService } from '../gitService'; +import { Logger } from '../logger'; +import * as path from 'path'; + +export interface DiffWithCommandArgsRevision { + sha: string; + uri: Uri; + title?: string; +} + +export interface DiffWithCommandArgs { + lhs?: DiffWithCommandArgsRevision; + rhs?: DiffWithCommandArgsRevision; + repoPath?: string; + + line?: number; + showOptions?: TextDocumentShowOptions; +} + +export class DiffWithCommand extends ActiveEditorCommand { + + static getMarkdownCommandArgs(args: DiffWithCommandArgs): string; + static getMarkdownCommandArgs(commit1: GitCommit, commit2: GitCommit): string; + static getMarkdownCommandArgs(argsOrCommit1: DiffWithCommandArgs | GitCommit, commit2?: GitCommit): string { + let args = argsOrCommit1; + if (argsOrCommit1 instanceof GitCommit) { + const commit1 = argsOrCommit1; + + if (commit2 === undefined) { + if (commit1.isUncommitted) { + args = { + repoPath: commit1.repoPath, + lhs: { + sha: commit1.sha, + uri: commit1.uri + }, + rhs: { + sha: 'HEAD', + uri: commit1.uri + } + }; + } + else { + args = { + repoPath: commit1.repoPath, + lhs: { + sha: commit1.previousSha!, + uri: commit1.previousUri! + }, + rhs: { + sha: commit1.sha, + uri: commit1.uri + } + }; + } + } + else { + args = { + repoPath: commit1.repoPath, + lhs: { + sha: commit1.sha, + uri: commit1.uri + }, + rhs: { + sha: commit2.sha, + uri: commit2.uri + } + }; + } + } + + return super.getMarkdownCommandArgsCore(Commands.DiffWith, args); + } + + constructor(private git: GitService) { + super(Commands.DiffWith); + } + + async execute(editor?: TextEditor, uri?: Uri, args: DiffWithCommandArgs = {}): Promise { + if (args.repoPath === undefined || args.lhs === undefined || args.rhs === undefined) return undefined; + + if (args.lhs.title === undefined) { + args.lhs.title = (args.lhs.sha === 'HEAD') + ? `${path.basename(args.lhs.uri.fsPath)}` + : `${path.basename(args.lhs.uri.fsPath)} (${GitService.shortenSha(args.lhs.sha)})`; + } + if (args.rhs.title === undefined) { + args.rhs.title = (args.rhs.sha === 'HEAD') + ? `${path.basename(args.rhs.uri.fsPath)}` + : `${path.basename(args.rhs.uri.fsPath)} (${GitService.shortenSha(args.rhs.sha)})`; + } + + try { + const [lhs, rhs] = await Promise.all([ + args.lhs.sha !== 'HEAD' + ? this.git.getVersionedFile(args.repoPath, args.lhs.uri.fsPath, args.lhs.sha) + : args.lhs.uri.fsPath, + args.rhs.sha !== 'HEAD' + ? this.git.getVersionedFile(args.repoPath, args.rhs.uri.fsPath, args.rhs.sha) + : args.rhs.uri.fsPath + ]); + + if (args.line !== undefined && args.line !== 0) { + if (args.showOptions === undefined) { + args.showOptions = {}; + } + args.showOptions.selection = new Range(args.line, 0, args.line, 0); + } + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(lhs), + Uri.file(rhs), + `${args.lhs.title} ${GlyphChars.ArrowLeftRight} ${args.rhs.title}`, + args.showOptions); + } + catch (ex) { + Logger.error(ex, 'DiffWithCommand', 'getVersionedFile'); + return window.showErrorMessage(`Unable to open compare. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index dc8f683..9d8d104 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -20,6 +20,15 @@ export interface ShowQuickCommitDetailsCommandArgs { export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { + static getMarkdownCommandArgs(sha: string): string; + static getMarkdownCommandArgs(args: ShowQuickCommitDetailsCommandArgs): string; + static getMarkdownCommandArgs(argsOrSha: ShowQuickCommitDetailsCommandArgs | string): string { + const args = typeof argsOrSha === 'string' + ? { sha: argsOrSha } + : argsOrSha; + return super.getMarkdownCommandArgsCore(Commands.ShowQuickCommitDetails, args); + } + constructor(private git: GitService) { super(Commands.ShowQuickCommitDetails); } diff --git a/src/commands/showQuickCommitFileDetails.ts b/src/commands/showQuickCommitFileDetails.ts index f55aa9b..3930cc8 100644 --- a/src/commands/showQuickCommitFileDetails.ts +++ b/src/commands/showQuickCommitFileDetails.ts @@ -20,6 +20,15 @@ export interface ShowQuickCommitFileDetailsCommandArgs { export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand { + static getMarkdownCommandArgs(sha: string): string; + static getMarkdownCommandArgs(args: ShowQuickCommitFileDetailsCommandArgs): string; + static getMarkdownCommandArgs(argsOrSha: ShowQuickCommitFileDetailsCommandArgs | string): string { + const args = typeof argsOrSha === 'string' + ? { sha: argsOrSha } + : argsOrSha; + return super.getMarkdownCommandArgsCore(Commands.ShowQuickCommitFileDetails, args); + } + constructor(private git: GitService) { super(Commands.ShowQuickCommitFileDetails); } diff --git a/src/extension.ts b/src/extension.ts index b74b6f1..450b23c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import { AnnotationController } from './annotations/annotationController'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { OpenBranchesInRemoteCommand, OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; -import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands'; +import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands'; import { ResetSuppressedWarningsCommand } from './commands'; import { ClearFileAnnotationsCommand, ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; @@ -103,6 +103,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new DiffDirectoryCommand(git)); context.subscriptions.push(new DiffLineWithPreviousCommand(git)); context.subscriptions.push(new DiffLineWithWorkingCommand(git)); + context.subscriptions.push(new DiffWithCommand(git)); context.subscriptions.push(new DiffWithBranchCommand(git)); context.subscriptions.push(new DiffWithNextCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git));