diff --git a/src/commands/diffWithRevision.ts b/src/commands/diffWithRevision.ts index 58b47ac..2dd9926 100644 --- a/src/commands/diffWithRevision.ts +++ b/src/commands/diffWithRevision.ts @@ -10,6 +10,7 @@ import { CommandQuickPickItem, CommitPicker } from '../quickpicks'; import { Strings } from '../system'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; import { DiffWithCommandArgs } from './diffWith'; +import { DiffWithRevisionFromCommandArgs } from './diffWithRevisionFrom'; export interface DiffWithRevisionCommandArgs { line?: number; @@ -70,10 +71,14 @@ export class DiffWithRevisionCommand extends ActiveEditorCommand { showOptions: args!.showOptions, })); }, - showOtherReferences: CommandQuickPickItem.fromCommand( - 'Choose a branch or tag...', - Commands.DiffWithRevisionFrom, - ), + showOtherReferences: [ + CommandQuickPickItem.fromCommand('Choose a Branch or Tag...', Commands.DiffWithRevisionFrom), + CommandQuickPickItem.fromCommand( + 'Choose a Stash...', + Commands.DiffWithRevisionFrom, + { stash: true }, + ), + ], }, ); if (pick == null) return; diff --git a/src/commands/diffWithRevisionFrom.ts b/src/commands/diffWithRevisionFrom.ts index 97715c2..e4aa193 100644 --- a/src/commands/diffWithRevisionFrom.ts +++ b/src/commands/diffWithRevisionFrom.ts @@ -6,7 +6,7 @@ import { Container } from '../container'; import { GitReference, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; import { Messages } from '../messages'; -import { ReferencePicker } from '../quickpicks'; +import { ReferencePicker, StashPicker } from '../quickpicks'; import { Strings } from '../system'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; import { DiffWithCommandArgs } from './diffWith'; @@ -14,6 +14,7 @@ import { DiffWithCommandArgs } from './diffWith'; export interface DiffWithRevisionFromCommandArgs { line?: number; showOptions?: TextDocumentShowOptions; + stash?: boolean; } @command() @@ -38,19 +39,42 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand { args.line = editor?.selection.active.line ?? 0; } - const title = `Open Changes with Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}`; - const pick = await ReferencePicker.show( - gitUri.repoPath, - `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, - 'Choose a branch or tag to compare with', - { - allowEnteringRefs: true, - // checkmarks: false, - }, - ); - if (pick == null) return; + let ref; + let sha; + if (args?.stash) { + const fileName = Strings.normalizePath(paths.relative(gitUri.repoPath, gitUri.fsPath)); + + const title = `Open Changes with Stash${Strings.pad(GlyphChars.Dot, 2, 2)}`; + const pick = await StashPicker.show( + Container.instance.git.getStash(gitUri.repoPath), + `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, + 'Choose a stash to compare with', + { + empty: `No stashes with '${gitUri.getFormattedFileName()}' found`, + filter: c => c.files.some(f => f.fileName === fileName || f.originalFileName === fileName), + }, + ); + if (pick == null) return; + + ref = pick.ref; + sha = ref; + } else { + const title = `Open Changes with Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}`; + const pick = await ReferencePicker.show( + gitUri.repoPath, + `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, + 'Choose a branch or tag to compare with', + { + allowEnteringRefs: true, + // checkmarks: false, + }, + ); + if (pick == null) return; + + ref = pick.ref; + sha = GitReference.isBranch(pick) && pick.remote ? `remotes/${ref}` : ref; + } - const ref = pick.ref; if (ref == null) return; let renamedUri: Uri | undefined; @@ -70,7 +94,7 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand { void (await executeCommand(Commands.DiffWith, { repoPath: gitUri.repoPath, lhs: { - sha: GitReference.isBranch(pick) && pick.remote ? `remotes/${ref}` : ref, + sha: sha, uri: renamedUri ?? gitUri, title: renamedTitle ?? `${paths.basename(gitUri.fsPath)} (${GitRevision.shorten(ref)})`, }, diff --git a/src/commands/openFileAtRevision.ts b/src/commands/openFileAtRevision.ts index d667741..1ed0d2d 100644 --- a/src/commands/openFileAtRevision.ts +++ b/src/commands/openFileAtRevision.ts @@ -11,6 +11,7 @@ import { CommandQuickPickItem, CommitPicker } from '../quickpicks'; import { Strings } from '../system'; import { ActiveEditorCommand, command, CommandContext, Commands, getCommandUri } from './common'; import { GitActions } from './gitCommands'; +import { OpenFileAtRevisionFromCommandArgs } from './openFileAtRevisionFrom'; export interface OpenFileAtRevisionCommandArgs { revisionUri?: Uri; @@ -115,10 +116,17 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand { preview: false, })); }, - showOtherReferences: CommandQuickPickItem.fromCommand( - 'Choose a branch or tag...', - Commands.OpenFileAtRevisionFrom, - ), + showOtherReferences: [ + CommandQuickPickItem.fromCommand( + 'Choose a Branch or Tag...', + Commands.OpenFileAtRevisionFrom, + ), + CommandQuickPickItem.fromCommand( + 'Choose a Stash...', + Commands.OpenFileAtRevisionFrom, + { stash: true }, + ), + ], }, ); if (pick == null) return; diff --git a/src/commands/openFileAtRevisionFrom.ts b/src/commands/openFileAtRevisionFrom.ts index 3ce6bf6..5995edb 100644 --- a/src/commands/openFileAtRevisionFrom.ts +++ b/src/commands/openFileAtRevisionFrom.ts @@ -1,11 +1,13 @@ 'use strict'; +import * as paths from 'path'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; +import { Container } from '../container'; import { GitReference } from '../git/git'; import { GitUri } from '../git/gitUri'; import { Messages } from '../messages'; -import { ReferencePicker } from '../quickpicks'; +import { ReferencePicker, StashPicker } from '../quickpicks'; import { Strings } from '../system'; import { ActiveEditorCommand, command, Commands, getCommandUri } from './common'; import { GitActions } from './gitCommands'; @@ -16,6 +18,7 @@ export interface OpenFileAtRevisionFromCommandArgs { line?: number; showOptions?: TextDocumentShowOptions; annotationType?: FileAnnotationType; + stash?: boolean; } @command() @@ -40,33 +43,48 @@ export class OpenFileAtRevisionFromCommand extends ActiveEditorCommand { } if (args.reference == null) { - const title = `Open File at Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}`; - const pick = await ReferencePicker.show( - gitUri.repoPath, - `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, - 'Choose a branch or tag to open the file revision from', - { - allowEnteringRefs: true, - keys: ['right', 'alt+right', 'ctrl+right'], - onDidPressKey: async (key, quickpick) => { - const [item] = quickpick.activeItems; - if (item != null) { - void (await GitActions.Commit.openFileAtRevision( - GitUri.toRevisionUri(item.ref, gitUri.fsPath, gitUri.repoPath!), - { - annotationType: args!.annotationType, - line: args!.line, - preserveFocus: true, - preview: false, - }, - )); - } + if (args?.stash) { + const fileName = Strings.normalizePath(paths.relative(gitUri.repoPath, gitUri.fsPath)); + + const title = `Open Changes with Stash${Strings.pad(GlyphChars.Dot, 2, 2)}`; + const pick = await StashPicker.show( + Container.instance.git.getStash(gitUri.repoPath), + `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, + 'Choose a stash to compare with', + { filter: c => c.files.some(f => f.fileName === fileName || f.originalFileName === fileName) }, + ); + if (pick == null) return; + + args.reference = pick; + } else { + const title = `Open File at Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}`; + const pick = await ReferencePicker.show( + gitUri.repoPath, + `${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`, + 'Choose a branch or tag to open the file revision from', + { + allowEnteringRefs: true, + keys: ['right', 'alt+right', 'ctrl+right'], + onDidPressKey: async (key, quickpick) => { + const [item] = quickpick.activeItems; + if (item != null) { + void (await GitActions.Commit.openFileAtRevision( + GitUri.toRevisionUri(item.ref, gitUri.fsPath, gitUri.repoPath!), + { + annotationType: args!.annotationType, + line: args!.line, + preserveFocus: true, + preview: false, + }, + )); + } + }, }, - }, - ); - if (pick == null) return; + ); + if (pick == null) return; - args.reference = pick; + args.reference = pick; + } } void (await GitActions.Commit.openFileAtRevision( diff --git a/src/quickpicks/commitPicker.ts b/src/quickpicks/commitPicker.ts index 292592c..c658ac4 100644 --- a/src/quickpicks/commitPicker.ts +++ b/src/quickpicks/commitPicker.ts @@ -2,7 +2,7 @@ import { Disposable, window } from 'vscode'; import { configuration } from '../configuration'; import { Container } from '../container'; -import { GitLog, GitLogCommit } from '../git/git'; +import { GitLog, GitLogCommit, GitStash, GitStashCommit } from '../git/git'; import { KeyboardScope, Keys } from '../keyboard'; import { CommandQuickPickItem, @@ -22,7 +22,7 @@ export namespace CommitPicker { picked?: string; keys?: Keys[]; onDidPressKey?(key: Keys, item: CommitQuickPickItem): void | Promise; - showOtherReferences?: CommandQuickPickItem; + showOtherReferences?: CommandQuickPickItem[]; }, ): Promise { const quickpick = window.createQuickPick(); @@ -55,7 +55,7 @@ export namespace CommitPicker { return log == null ? [DirectiveQuickPickItem.create(Directive.Cancel)] : [ - ...(options?.showOtherReferences != null ? [options?.showOtherReferences] : []), + ...(options?.showOtherReferences ?? []), ...Iterables.map(log.commits.values(), commit => CommitQuickPickItem.create(commit, options?.picked === commit.ref, { compact: true, @@ -181,3 +181,139 @@ export namespace CommitPicker { } } } + +export namespace StashPicker { + export async function show( + stash: GitStash | undefined | Promise, + title: string, + placeholder: string, + options?: { + empty?: string; + filter?: (c: GitStashCommit) => boolean; + keys?: Keys[]; + onDidPressKey?(key: Keys, item: CommitQuickPickItem): void | Promise; + picked?: string; + showOtherReferences?: CommandQuickPickItem[]; + }, + ): Promise { + const quickpick = window.createQuickPick< + CommandQuickPickItem | CommitQuickPickItem | DirectiveQuickPickItem + >(); + quickpick.ignoreFocusOut = getQuickPickIgnoreFocusOut(); + + quickpick.title = title; + quickpick.placeholder = placeholder; + quickpick.matchOnDescription = true; + quickpick.matchOnDetail = true; + + if (Promises.is(stash)) { + quickpick.busy = true; + quickpick.enabled = false; + quickpick.show(); + + stash = await stash; + } + + if (stash != null) { + quickpick.items = [ + ...(options?.showOtherReferences ?? []), + ...Iterables.map( + options?.filter != null + ? Iterables.filter(stash.commits.values(), options.filter) + : stash.commits.values(), + commit => + CommitQuickPickItem.create(commit, options?.picked === commit.ref, { + compact: true, + icon: true, + }), + ), + ]; + } + + if (stash == null || quickpick.items.length <= (options?.showOtherReferences?.length ?? 0)) { + quickpick.placeholder = stash == null ? 'No stashes found' : options?.empty ?? `No matching stashes found`; + quickpick.items = [DirectiveQuickPickItem.create(Directive.Cancel)]; + } + + if (options?.picked) { + quickpick.activeItems = quickpick.items.filter(i => (CommandQuickPickItem.is(i) ? false : i.picked)); + } + + const disposables: Disposable[] = []; + + let scope: KeyboardScope | undefined; + if (options?.keys != null && options.keys.length !== 0 && options?.onDidPressKey !== null) { + scope = Container.instance.keyboard.createScope( + Object.fromEntries( + options.keys.map(key => [ + key, + { + onDidPressKey: key => { + if (quickpick.activeItems.length !== 0) { + const [item] = quickpick.activeItems; + if ( + item != null && + !DirectiveQuickPickItem.is(item) && + !CommandQuickPickItem.is(item) + ) { + void options.onDidPressKey!(key, item); + } + } + }, + }, + ]), + ), + ); + void scope.start(); + disposables.push(scope); + } + + try { + const pick = await new Promise< + CommandQuickPickItem | CommitQuickPickItem | DirectiveQuickPickItem | undefined + >(resolve => { + disposables.push( + quickpick.onDidHide(() => resolve(undefined)), + quickpick.onDidAccept(() => { + if (quickpick.activeItems.length !== 0) { + const [item] = quickpick.activeItems; + if (DirectiveQuickPickItem.is(item)) { + resolve(undefined); + return; + } + + resolve(item); + } + }), + quickpick.onDidChangeValue(async e => { + if (scope == null) return; + + // Pause the left/right keyboard commands if there is a value, otherwise the left/right arrows won't work in the input properly + if (e.length !== 0) { + await scope.pause(['left', 'right']); + } else { + await scope.resume(); + } + }), + ); + + quickpick.busy = false; + quickpick.enabled = true; + + quickpick.show(); + }); + if (pick == null || DirectiveQuickPickItem.is(pick)) return undefined; + + if (pick instanceof CommandQuickPickItem) { + void (await pick.execute()); + + return undefined; + } + + return pick.item; + } finally { + quickpick.dispose(); + disposables.forEach(d => d.dispose()); + } + } +}