diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index cc548d0..a0a7a72 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -13,7 +13,7 @@ import { } from '../../git/gitService'; import { GlyphChars } from '../../constants'; import { QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand'; -import { RepositoryQuickPickItem } from '../../quickpicks'; +import { CommandQuickPickItem, CommitQuickPick, RepositoryQuickPickItem } from '../../quickpicks'; import { Iterables, Mutable, Strings } from '../../system'; import { Logger } from '../../logger'; import { @@ -23,12 +23,8 @@ import { QuickPickItemOfT } from '../../quickpicks/gitQuickPicks'; -interface State { +interface State extends Required { repo: Repository; - search: string; - matchAll: boolean; - matchCase: boolean; - matchRegex: boolean; showInView: boolean; } @@ -141,7 +137,7 @@ export class SearchGitCommand extends QuickCommandBase { counter++; } - if (args.state.search !== undefined && !args.prefillOnly) { + if (args.state.pattern !== undefined && !args.prefillOnly) { counter++; } @@ -195,7 +191,6 @@ export class SearchGitCommand extends QuickCommandBase { const active = state.repo ? state.repo : await Container.git.getActiveRepository(); const step = this.createPickStep({ - multiselect: true, title: this.title, placeholder: 'Choose repositories', items: await Promise.all( @@ -218,7 +213,7 @@ export class SearchGitCommand extends QuickCommandBase { } } - if (state.search === undefined || state.counter < 2) { + if (state.pattern === undefined || state.counter < 2) { const items: QuickPickItemOfT[] = [ { label: searchOperatorToTitleMap.get('')!, @@ -268,7 +263,7 @@ export class SearchGitCommand extends QuickCommandBase { matchOnDetail: true, additionalButtons: [matchCaseButton, matchAllButton, matchRegexButton, showInViewButton], items: items, - value: state.search, + value: state.pattern, onDidAccept: (quickpick): boolean => { const pick = quickpick.selectedItems[0]; if (!searchOperators.has(pick.item)) return true; @@ -352,11 +347,11 @@ export class SearchGitCommand extends QuickCommandBase { continue; } - state.search = selection[0].item.trim(); + state.pattern = selection[0].item.trim(); } const search: SearchPattern = { - pattern: state.search, + pattern: state.pattern, matchAll: state.matchAll, matchCase: state.matchCase, matchRegex: state.matchRegex @@ -373,7 +368,7 @@ export class SearchGitCommand extends QuickCommandBase { state.repo.path, search, { - label: { label: `for ${state.search}` } + label: { label: `for ${state.pattern}` } }, resultsPromise ); @@ -387,10 +382,10 @@ export class SearchGitCommand extends QuickCommandBase { title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`, placeholder: results === undefined - ? `No results for ${state.search}` + ? `No results for ${state.pattern}` : `${Strings.pluralize('result', results.count, { number: results.truncated ? `${results.count}+` : undefined - })} for ${state.search}`, + })} for ${state.pattern}`, matchOnDescription: true, matchOnDetail: true, items: @@ -416,10 +411,31 @@ export class SearchGitCommand extends QuickCommandBase { state.repo!.path, search, { - label: { label: `for ${state.search}` } + label: { label: `for ${state.pattern}` } }, results ); + }, + keys: ['right', 'alt+right', 'ctrl+right'], + onDidPressKey: async (quickpick, key) => { + if (quickpick.activeItems.length === 0) return; + + const commit = quickpick.activeItems[0].item; + if (key === 'ctrl+right') { + await Container.repositoriesView.revealCommit(commit, { + select: true, + focus: false, + expand: true + }); + } else { + await Container.searchView.search( + commit.repoPath, + { pattern: SearchPattern.fromCommit(commit) }, + { + label: { label: `for commit id ${commit.shortSha}` } + } + ); + } } }); const selection: StepSelection = yield step; @@ -428,38 +444,48 @@ export class SearchGitCommand extends QuickCommandBase { continue; } - state.counter--; pickedCommit = selection[0].item; - void Container.searchView.search( - pickedCommit.repoPath, - { pattern: `commit:${pickedCommit.sha}` }, - { - label: { label: `for commit id ${pickedCommit.shortSha}` } + if (pickedCommit !== undefined) { + const step = this.createPickStep({ + title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }${Strings.pad(GlyphChars.Dot, 2, 2)}${pickedCommit.shortSha}`, + placeholder: `${pickedCommit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${ + pickedCommit.author ? `${pickedCommit.author}, ` : '' + }${pickedCommit.formattedDate} ${Strings.pad( + GlyphChars.Dot, + 1, + 1 + )} ${pickedCommit.getShortMessage()}`, + items: await CommitQuickPick.getItems(pickedCommit, pickedCommit.toGitUri(), { + showChanges: false + }), + additionalButtons: [this.Buttons.OpenInView], + onDidClickButton: (quickpick, button) => { + if (button !== this.Buttons.OpenInView) return; + + void Container.searchView.search( + pickedCommit!.repoPath, + { pattern: SearchPattern.fromCommit(pickedCommit!) }, + { + label: { label: `for commit id ${pickedCommit!.shortSha}` } + } + ); + } + }); + const selection: StepSelection = yield step; + + if (!this.canPickStepMoveNext(step, state, selection)) { + continue; } - ); - - // const gitCommandArgs: GitCommandsCommandArgs = { - // command: 'search', - // state: { ...state } - // }; - - // const commandArgs: ShowQuickCommitDetailsCommandArgs = { - // sha: commit.sha, - // commit: commit, - // goBackCommand: new CommandQuickPickItem( - // { - // label: 'Back', - // description: '' - // }, - // Commands.GitCommands, - // [gitCommandArgs] - // ) - // }; - - // void commands.executeCommand(Commands.ShowQuickCommitDetails, commit.toGitUri(), commandArgs); - - // break; + + const command = selection[0]; + if (command instanceof CommandQuickPickItem) { + command.execute(); + break; + } + } } catch (ex) { Logger.error(ex, this.title); diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 79a07d3..39521a9 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -1,11 +1,12 @@ 'use strict'; /* eslint-disable no-loop-func */ -import { commands, QuickInputButton, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode'; +import { QuickInputButton, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode'; import { Container } from '../../container'; -import { GitStashCommit, GitUri, Repository } from '../../git/gitService'; +import { GitStashCommit, GitUri, Repository, SearchPattern } from '../../git/gitService'; import { BreakQuickCommand, QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand'; import { CommandQuickPickItem, + CommitQuickPick, CommitQuickPickItem, Directive, DirectiveQuickPickItem, @@ -17,8 +18,6 @@ import { Iterables, Strings } from '../../system'; import { GlyphChars } from '../../constants'; import { Logger } from '../../logger'; import { Messages } from '../../messages'; -import { GitCommandsCommandArgs, ShowQuickCommitDetailsCommandArgs } from '../../commands'; -import { Commands } from '../common'; interface ApplyState { subcommand: 'apply'; @@ -586,6 +585,17 @@ export class StashGitCommand extends QuickCommandBase { expand: true }); } + }, + keys: ['right', 'alt+right', 'ctrl+right'], + onDidPressKey: async (quickpick, key) => { + if (quickpick.activeItems.length === 0) return; + + const stash = quickpick.activeItems[0].item; + await Container.repositoriesView.revealStash(stash, { + select: true, + focus: false, + expand: true + }); } }); const selection: StepSelection = yield step; @@ -594,33 +604,43 @@ export class StashGitCommand extends QuickCommandBase { break; } - state.counter--; pickedStash = selection[0].item; - // const node = await Container.repositoriesView.findStashNode(pickedStash); - // if (node !== undefined) { - // Container.repositoriesView.reveal(node, { select: true, expand: true }); - // } + if (pickedStash !== undefined) { + const step = this.createPickStep({ + title: `${this.title} ${getSubtitle(state.subcommand)}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.repo.formattedName + }${Strings.pad(GlyphChars.Dot, 2, 2)}${pickedStash.shortSha}`, + placeholder: `${ + pickedStash.number === undefined ? '' : `${pickedStash.number}: ` + }${pickedStash.getShortMessage()}`, + items: await CommitQuickPick.getItems(pickedStash, pickedStash.toGitUri(), { showChanges: false }), + additionalButtons: [this.Buttons.OpenInView], + onDidClickButton: (quickpick, button) => { + if (button !== this.Buttons.OpenInView) return; + + void Container.searchView.search( + pickedStash!.repoPath, + { pattern: SearchPattern.fromCommit(pickedStash!) }, + { + label: { label: `for commit id ${pickedStash!.shortSha}` } + } + ); + } + }); + const selection: StepSelection = yield step; + + if (!this.canPickStepMoveNext(step, state, selection)) { + continue; + } - const gitCommandArgs: GitCommandsCommandArgs = { - command: 'stash', - state: { ...state } - }; + const command = selection[0]; + if (command instanceof CommandQuickPickItem) { + command.execute(); - const commandArgs: ShowQuickCommitDetailsCommandArgs = { - sha: pickedStash.sha, - commit: pickedStash, - goBackCommand: new CommandQuickPickItem( - { - label: 'Back', - description: '' - }, - Commands.GitCommands, - [gitCommandArgs] - ) - }; - - void commands.executeCommand(Commands.ShowQuickCommitDetails, pickedStash.toGitUri(), commandArgs); + throw new BreakQuickCommand(); + } + } } return undefined; diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index fedf968..46b89a8 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -24,6 +24,7 @@ import { StashGitCommand, StashGitCommandArgs } from './git/stash'; import { SwitchGitCommand, SwitchGitCommandArgs } from './git/switch'; import { Container } from '../container'; import { configuration } from '../configuration'; +import { KeyMapping } from '../keyboard'; const sanitizeLabel = /\$\(.+?\)|\s/g; @@ -140,18 +141,18 @@ export class GitCommandsCommand extends Command { } }; - const scope = Container.keyboard.createScope({ - left: { onDidPressKey: goBack }, - right: { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key) - }, - 'ctrl+right': { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key) - }, - 'alt+right': { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key) + const mapping: KeyMapping = { + left: { onDidPressKey: goBack } + }; + if (step.onDidPressKey !== undefined && step.keys !== undefined && step.keys.length !== 0) { + for (const key of step.keys) { + mapping[key] = { + onDidPressKey: key => step.onDidPressKey!(input, key) + }; } - }); + } + + const scope = Container.keyboard.createScope(mapping); scope.start(); disposables.push( @@ -234,18 +235,18 @@ export class GitCommandsCommand extends Command { } }; - const scope = Container.keyboard.createScope({ - left: { onDidPressKey: goBack }, - right: { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key) - }, - 'ctrl+right': { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key) - }, - 'alt+right': { - onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key) + const mapping: KeyMapping = { + left: { onDidPressKey: goBack } + }; + if (step.onDidPressKey !== undefined && step.keys !== undefined && step.keys.length !== 0) { + for (const key of step.keys) { + mapping[key] = { + onDidPressKey: key => step.onDidPressKey!(quickpick, key) + }; } - }); + } + + const scope = Container.keyboard.createScope(mapping); scope.start(); let overrideItems = false; diff --git a/src/commands/quickCommand.ts b/src/commands/quickCommand.ts index 2a921c7..a8120c5 100644 --- a/src/commands/quickCommand.ts +++ b/src/commands/quickCommand.ts @@ -15,6 +15,7 @@ export class BreakQuickCommand extends Error { export interface QuickInputStep { additionalButtons?: QuickInputButton[]; buttons?: QuickInputButton[]; + keys?: StepNavigationKeys[]; placeholder?: string; title?: string; value?: string; @@ -31,12 +32,13 @@ export function isQuickInputStep(item: QuickPickStep | QuickInputStep): item is export interface QuickPickStep { additionalButtons?: QuickInputButton[]; buttons?: QuickInputButton[]; - selectedItems?: QuickPickItem[]; items: (DirectiveQuickPickItem | T)[] | DirectiveQuickPickItem[]; + keys?: StepNavigationKeys[]; matchOnDescription?: boolean; matchOnDetail?: boolean; multiselect?: boolean; placeholder?: string; + selectedItems?: QuickPickItem[]; title?: string; value?: string; @@ -54,6 +56,7 @@ export function isQuickPickStep(item: QuickPickStep | QuickInputStep): item is Q export type StepAsyncGenerator = AsyncGenerator; type StepItemType = T extends QuickPickStep ? U[] : T extends QuickInputStep ? string : never; +export type StepNavigationKeys = Exclude, 'alt+left'>, 'ctrl+left'>; export type StepSelection = T extends QuickPickStep ? U[] | Directive : T extends QuickInputStep diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index 410f638..ca5a4c1 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, Uri } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitLog, GitLogCommit, GitUri } from '../git/gitService'; +import { GitCommit, GitLog, GitLogCommit, GitUri, SearchPattern } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitQuickPick, CommitWithFileStatusQuickPickItem } from '../quickpicks'; @@ -122,7 +122,7 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { if (args.showInView) { void (await Container.searchView.search( repoPath!, - { pattern: `commit:${args.commit.sha}` }, + { pattern: SearchPattern.fromCommit(args.commit) }, { label: { label: `for commit id ${args.commit.shortSha}` } } diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 2f5e323..e5eabf2 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -8,8 +8,8 @@ import { Messages } from '../messages'; import { CommandQuickPickItem, FileHistoryQuickPick, - ShowFileHistoryFromQuickPickItem, - ShowFileHistoryInViewQuickPickItem + OpenFileHistoryInViewQuickPickItem, + ShowFileHistoryFromQuickPickItem } from '../quickpicks'; import { Iterables, Strings } from '../system'; import { ActiveEditorCachedCommand, command, CommandContext, Commands, getCommandUri } from './common'; @@ -148,7 +148,7 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand { : undefined, showInViewCommand: args.log !== undefined - ? new ShowFileHistoryInViewQuickPickItem( + ? new OpenFileHistoryInViewQuickPickItem( gitUri, (args.reference && args.reference.ref) || gitUri.sha ) diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 209d302..5e0f6c6 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -40,6 +40,7 @@ import { GitBlameParser, GitBranch, GitBranchParser, + GitCommit, GitCommitType, GitContributor, GitDiff, @@ -129,6 +130,12 @@ export interface SearchPattern { } export namespace SearchPattern { + export function fromCommit(ref: string): string; + export function fromCommit(commit: GitCommit): string; + export function fromCommit(refOrCommit: string | GitCommit) { + return `commit:${GitService.shortenSha(typeof refOrCommit === 'string' ? refOrCommit : refOrCommit.sha)}`; + } + export function toKey(search: SearchPattern) { return `${search.pattern}|${search.matchAll ? 'A' : ''}${search.matchCase ? 'C' : ''}${ search.matchRegex ? 'R' : '' diff --git a/src/quickpicks/commitFileQuickPick.ts b/src/quickpicks/commitFileQuickPick.ts index e4f2bea..8bba8bb 100644 --- a/src/quickpicks/commitFileQuickPick.ts +++ b/src/quickpicks/commitFileQuickPick.ts @@ -229,7 +229,7 @@ export class CommitFileQuickPick { new CommandQuickPickItem( { label: '$(clippy) Copy Commit ID to Clipboard', - description: `${commit.shortSha}` + description: '' }, Commands.CopyShaToClipboard, [uri, copyShaCommandArgs] @@ -243,8 +243,8 @@ export class CommitFileQuickPick { items.push( new CommandQuickPickItem( { - label: '$(clippy) Copy Commit Message to Clipboard', - description: `${commit.getShortMessage()}` + label: `$(clippy) Copy ${commit.isStash ? 'Stash' : 'Commit'} Message to Clipboard`, + description: '' }, Commands.CopyMessageToClipboard, [uri, copyMessageCommandArgs] diff --git a/src/quickpicks/commitQuickPick.ts b/src/quickpicks/commitQuickPick.ts index 003f3a9..2dbd770 100644 --- a/src/quickpicks/commitQuickPick.ts +++ b/src/quickpicks/commitQuickPick.ts @@ -31,8 +31,9 @@ import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, + OpenCommitInViewQuickPickItem, QuickPickItem, - ShowCommitInViewQuickPickItem + RevealCommitInViewQuickPickItem } from './commonQuickPicks'; import { OpenRemotesCommandQuickPickItem } from './remotesQuickPick'; @@ -146,6 +147,7 @@ export interface CommitQuickPickOptions { currentCommand?: CommandQuickPickItem; goBackCommand?: CommandQuickPickItem; repoLog?: GitLog; + showChanges?: boolean; } export class CommitQuickPick { @@ -156,6 +158,8 @@ export class CommitQuickPick { uri: Uri, options: CommitQuickPickOptions = {} ): Promise { + options = { showChanges: true, ...options }; + let previousCommand: (() => Promise) | undefined = undefined; let nextCommand: (() => Promise) | undefined = undefined; if (!commit.isStash) { @@ -220,7 +224,7 @@ export class CommitQuickPick { 'alt+.': nextCommand }); - const pick = await window.showQuickPick(this.getItems(commit, uri, options), { + const pick = await window.showQuickPick(CommitQuickPick.getItems(commit, uri, options), { matchOnDescription: true, matchOnDetail: true, placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${ @@ -240,29 +244,23 @@ export class CommitQuickPick { return pick; } - private async getItems(commit: GitLogCommit, uri: Uri, options: CommitQuickPickOptions = {}) { - const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.files.map( - fs => new CommitWithFileStatusQuickPickItem(commit, fs) - ); - - const stash = commit.isStash; - - let index = 0; + static async getItems(commit: GitLogCommit, uri: Uri, options: CommitQuickPickOptions = {}) { + const items: CommandQuickPickItem[] = []; let remotes; - if (stash) { + if (GitStashCommit.is(commit)) { const stashApplyCommmandArgs: StashApplyCommandArgs = { deleteAfter: false, - stashItem: commit as GitStashCommit, + stashItem: commit, goBackCommand: options.currentCommand }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(git-pull-request) Apply Stash', - description: `${commit.getShortMessage()}` + description: `${ + commit.number === undefined ? '' : `${commit.number}: ` + }${commit.getShortMessage()}` }, Commands.StashApply, [stashApplyCommmandArgs] @@ -270,31 +268,33 @@ export class CommitQuickPick { ); const stashDeleteCommmandArgs: StashDeleteCommandArgs = { - stashItem: commit as GitStashCommit, + stashItem: commit, goBackCommand: options.currentCommand }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(x) Delete Stash', - description: `${commit.getShortMessage()}` + description: `${ + commit.number === undefined ? '' : `${commit.number}: ` + }${commit.getShortMessage()}` }, Commands.StashDelete, [stashDeleteCommmandArgs] ) ); - items.splice(index++, 0, new ShowCommitInViewQuickPickItem(commit)); + items.push(new OpenCommitInViewQuickPickItem(commit)); + items.push(new RevealCommitInViewQuickPickItem(commit)); } else { - items.splice(index++, 0, new ShowCommitInViewQuickPickItem(commit)); + items.push(new OpenCommitInViewQuickPickItem(commit)); + items.push(new RevealCommitInViewQuickPickItem(commit)); + items.push(new OpenCommitFilesCommandQuickPickItem(commit)); + items.push(new OpenCommitFileRevisionsCommandQuickPickItem(commit)); remotes = await Container.git.getRemotes(commit.repoPath, { sort: true }); if (remotes.length) { - items.splice( - index++, - 0, + items.push( new OpenRemotesCommandQuickPickItem( remotes, { @@ -307,18 +307,13 @@ export class CommitQuickPick { } } - items.splice(index++, 0, new OpenCommitFilesCommandQuickPickItem(commit)); - items.splice(index++, 0, new OpenCommitFileRevisionsCommandQuickPickItem(commit)); - const previousSha = await Container.git.resolveReference(commit.repoPath, commit.previousFileSha); let diffDirectoryCommmandArgs: DiffDirectoryCommandArgs = { ref1: previousSha, ref2: commit.sha }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(git-compare) Open Directory Compare with Previous Revision', @@ -334,9 +329,7 @@ export class CommitQuickPick { diffDirectoryCommmandArgs = { ref1: commit.sha }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(git-compare) Open Directory Compare with Working Tree', @@ -347,17 +340,15 @@ export class CommitQuickPick { ) ); - if (!stash) { + if (!GitStashCommit.is(commit)) { const copyShaCommandArgs: CopyShaToClipboardCommandArgs = { sha: commit.sha }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(clippy) Copy Commit ID to Clipboard', - description: `${commit.shortSha}` + description: '' }, Commands.CopyShaToClipboard, [uri, copyShaCommandArgs] @@ -369,27 +360,23 @@ export class CommitQuickPick { message: commit.message, sha: commit.sha }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { - label: '$(clippy) Copy Commit Message to Clipboard', - description: `${commit.getShortMessage()}` + label: `$(clippy) Copy ${commit.isStash ? 'Stash' : 'Commit'} Message to Clipboard`, + description: '' }, Commands.CopyMessageToClipboard, [uri, copyMessageCommandArgs] ) ); - if (!stash) { + if (!GitStashCommit.is(commit)) { if (remotes !== undefined && remotes.length) { const copyRemoteUrlCommandArgs: CopyRemoteFileUrlToClipboardCommandArgs = { sha: commit.sha }; - items.splice( - index++, - 0, + items.push( new CommandQuickPickItem( { label: '$(clippy) Copy Remote Url to Clipboard' @@ -401,24 +388,26 @@ export class CommitQuickPick { } } - const commitDetailsCommandArgs: ShowQuickCommitDetailsCommandArgs = { - commit: commit, - repoLog: options.repoLog, - sha: commit.sha, - goBackCommand: options.goBackCommand - }; - items.splice( - index++, - 0, - new CommandQuickPickItem( - { - label: 'Changed Files', - description: commit.getFormattedDiffStatus() - }, - Commands.ShowQuickCommitDetails, - [uri, commitDetailsCommandArgs] - ) - ); + if (options.showChanges) { + const commitDetailsCommandArgs: ShowQuickCommitDetailsCommandArgs = { + commit: commit, + repoLog: options.repoLog, + sha: commit.sha, + goBackCommand: options.goBackCommand + }; + items.push( + new CommandQuickPickItem( + { + label: 'Changed Files', + description: commit.getFormattedDiffStatus() + }, + Commands.ShowQuickCommitDetails, + [uri, commitDetailsCommandArgs] + ) + ); + + items.push(...commit.files.map(fs => new CommitWithFileStatusQuickPickItem(commit, fs))); + } if (options.goBackCommand) { items.splice(0, 0, options.goBackCommand); diff --git a/src/quickpicks/commonQuickPicks.ts b/src/quickpicks/commonQuickPicks.ts index 8e803e1..0d184e0 100644 --- a/src/quickpicks/commonQuickPicks.ts +++ b/src/quickpicks/commonQuickPicks.ts @@ -3,9 +3,10 @@ import { CancellationTokenSource, commands, QuickPickItem, window } from 'vscode import { Commands } from '../commands'; import { configuration } from '../configuration'; import { Container } from '../container'; -import { GitLog, GitLogCommit, GitStashCommit, GitUri, SearchPattern } from '../git/gitService'; +import { GitLogCommit, GitStashCommit, GitUri, SearchPattern } from '../git/gitService'; import { KeyMapping, Keys } from '../keyboard'; import { ReferencesQuickPick, ReferencesQuickPickItem } from './referencesQuickPick'; +import { GlyphChars } from '../constants'; export function getQuickPickIgnoreFocusOut() { return !configuration.get('advanced', 'quickPick', 'closeOnFocusOut'); @@ -97,67 +98,76 @@ export class MessageQuickPickItem extends CommandQuickPickItem { } } -export class ShowCommitInViewQuickPickItem extends CommandQuickPickItem { +export class OpenCommitInViewQuickPickItem extends CommandQuickPickItem { constructor( public readonly commit: GitLogCommit, item: QuickPickItem = { - label: '$(eye) Show in View', - description: `shows the ${commit.isStash ? 'stash' : 'commit'} in the Repositories view` + label: '$(eye) Open in Search Commits View', + description: '' } ) { super(item, undefined, undefined); } async execute(): Promise<{} | undefined> { - if (GitStashCommit.is(this.commit)) { - void (await Container.repositoriesView.revealStash(this.commit, { - select: true, - focus: true, - expand: true - })); - - return undefined; - } + void (await Container.searchView.search( + this.commit.repoPath, + { + pattern: SearchPattern.fromCommit(this.commit) + }, + { + label: { label: `for ${this.commit.isStash ? 'stash' : 'commit'} id ${this.commit.shortSha}` } + } + )); - const node = await Container.repositoriesView.revealCommit(this.commit, { - select: true, - focus: true, - expand: true - }); + return undefined; + } +} - if (node === undefined) { - // Fallback to commit search, if we can't find it - void (await Container.searchView.search( - this.commit.repoPath, - { pattern: `commit:${this.commit.sha}` }, - { - label: { label: `for commit id ${this.commit.shortSha}` } - } - )); +export class OpenFileHistoryInViewQuickPickItem extends CommandQuickPickItem { + constructor( + public readonly uri: GitUri, + public readonly baseRef: string | undefined, + item: QuickPickItem = { + label: '$(eye) Open in File History View', + description: 'shows the file history in the File History view' } + ) { + super(item, undefined, undefined); + } - return undefined; + async execute(): Promise<{} | undefined> { + return void (await Container.fileHistoryView.showHistoryForUri(this.uri, this.baseRef)); } } -export class ShowCommitSearchResultsInViewQuickPickItem extends CommandQuickPickItem { +export class RevealCommitInViewQuickPickItem extends CommandQuickPickItem { constructor( - public readonly search: SearchPattern, - public readonly results: GitLog, - public readonly resultsLabel: string | { label: string; resultsType?: { singular: string; plural: string } }, + public readonly commit: GitLogCommit | GitStashCommit, item: QuickPickItem = { - label: '$(eye) Show in View', - description: 'shows the search results in the Search Commits view' + label: '$(eye) Reveal in Repositories View', + description: `${commit.isStash ? '' : `${GlyphChars.Dash} this can take a while`}` } ) { super(item, undefined, undefined); } - execute(): Promise<{} | undefined> { - Container.searchView.showSearchResults(this.results.repoPath, this.search, this.results, { - label: this.resultsLabel - }); - return Promise.resolve(undefined); + async execute(): Promise<{} | undefined> { + if (GitStashCommit.is(this.commit)) { + void (await Container.repositoriesView.revealStash(this.commit, { + select: true, + focus: true, + expand: true + })); + } else { + void (await Container.repositoriesView.revealCommit(this.commit, { + select: true, + focus: true, + expand: true + })); + } + + return undefined; } } @@ -182,20 +192,3 @@ export class ShowFileHistoryFromQuickPickItem extends CommandQuickPickItem { }); } } - -export class ShowFileHistoryInViewQuickPickItem extends CommandQuickPickItem { - constructor( - public readonly uri: GitUri, - public readonly baseRef: string | undefined, - item: QuickPickItem = { - label: '$(eye) Show in View', - description: 'shows the file history in the File History view' - } - ) { - super(item, undefined, undefined); - } - - async execute(): Promise<{} | undefined> { - return void (await Container.fileHistoryView.showHistoryForUri(this.uri, this.baseRef)); - } -} diff --git a/src/views/nodes.ts b/src/views/nodes.ts index b246bc4..0882bbc 100644 --- a/src/views/nodes.ts +++ b/src/views/nodes.ts @@ -5,6 +5,7 @@ export * from './nodes/common'; export * from './nodes/viewNode'; export * from './nodes/branchesNode'; export * from './nodes/branchNode'; +export * from './nodes/branchOrTagFolderNode'; export * from './nodes/branchTrackingStatusNode'; export * from './nodes/commitFileNode'; export * from './nodes/commitNode'; diff --git a/src/views/repositoriesView.ts b/src/views/repositoriesView.ts index ff19bec..d5df4c4 100644 --- a/src/views/repositoriesView.ts +++ b/src/views/repositoriesView.ts @@ -1,15 +1,31 @@ 'use strict'; -import { commands, ConfigurationChangeEvent, Event, EventEmitter } from 'vscode'; +import { + CancellationToken, + commands, + ConfigurationChangeEvent, + Event, + EventEmitter, + ProgressLocation, + window +} from 'vscode'; import { configuration, RepositoriesViewConfig, ViewFilesLayout, ViewsConfig } from '../configuration'; import { CommandContext, setCommandContext, WorkspaceState } from '../constants'; import { Container } from '../container'; -import { BranchesNode, BranchNode, RepositoriesNode, RepositoryNode, StashesNode, StashNode, ViewNode } from './nodes'; +import { + BranchesNode, + BranchNode, + BranchOrTagFolderNode, + CompareBranchNode, + RepositoriesNode, + RepositoryNode, + StashesNode, + StashNode, + ViewNode +} from './nodes'; import { ViewBase } from './viewBase'; import { ViewShowBranchComparison } from '../config'; -import { CompareBranchNode } from './nodes/compareBranchNode'; import { GitLogCommit, GitStashCommit } from '../git/git'; - -const emptyArray = (Object.freeze([]) as any) as any[]; +import { GitService } from '../git/gitService'; export class RepositoriesView extends ViewBase { constructor() { @@ -115,50 +131,51 @@ export class RepositoriesView extends ViewBase { return { ...Container.config.views, ...Container.config.views.repositories }; } - findCommitNode(commit: GitLogCommit | { repoPath: string; ref: string }) { + findCommitNode(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) { const repoNodeId = RepositoryNode.getId(commit.repoPath); return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, { allowPaging: true, - maxDepth: 2, - getChildren: async n => { - // Only search for commit nodes in the same repo within the root BranchNode - if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) { - if (!n.id.startsWith(repoNodeId)) return emptyArray; - - if (n instanceof BranchNode) { - if (!n.root) return emptyArray; - } else if (!(n instanceof RepositoryNode) && !(n instanceof BranchesNode)) { - return emptyArray; - } + maxDepth: 6, + canTraverse: n => { + // Only search for commit nodes in the same repo within BranchNodes + if (n instanceof RepositoriesNode) return true; + + if ( + n instanceof RepositoryNode || + n instanceof BranchesNode || + n instanceof BranchOrTagFolderNode || + n instanceof BranchNode + ) { + return n.id.startsWith(repoNodeId); } - return n.getChildren(); - } + return false; + }, + token: token }); } - findStashNode(stash: GitStashCommit | { repoPath: string; ref: string }) { + findStashNode(stash: GitStashCommit | { repoPath: string; ref: string }, token?: CancellationToken) { const repoNodeId = RepositoryNode.getId(stash.repoPath); return this.findNode(StashNode.getId(stash.repoPath, stash.ref), { - maxDepth: 2, - getChildren: async n => { + maxDepth: 3, + canTraverse: n => { // Only search for stash nodes in the same repo within a StashesNode - if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) { - if (!n.id.startsWith(repoNodeId)) return emptyArray; + if (n instanceof RepositoriesNode) return true; - if (!(n instanceof RepositoryNode) && !(n instanceof StashesNode)) { - return emptyArray; - } + if (n instanceof RepositoryNode || n instanceof StashesNode) { + return n.id.startsWith(repoNodeId); } - return n.getChildren(); - } + return false; + }, + token: token }); } - async revealCommit( + revealCommit( commit: GitLogCommit | { repoPath: string; ref: string }, options?: { select?: boolean; @@ -166,28 +183,60 @@ export class RepositoriesView extends ViewBase { expand?: boolean | number; } ) { - const node = await this.findCommitNode(commit); - if (node !== undefined) { - await this.reveal(node, options); - } + return window.withProgress( + { + location: ProgressLocation.Notification, + title: `Revealing commit '${GitService.shortenSha(commit.ref)}' in the Repositories view...`, + cancellable: true + }, + async (progress, token) => { + const node = await this.findCommitNode(commit, token); + if (node === undefined) return node; + + // Not sure why I need to reveal each parent, but without it the node won't be revealed + const nodes: ViewNode[] = []; + + let parent: ViewNode | undefined = node; + while (parent !== undefined) { + nodes.push(parent); + parent = parent.getParent(); + } + nodes.pop(); - return node; + for (const n of nodes.reverse()) { + try { + await this.reveal(n, options); + } catch {} + } + + return node; + } + ); } async revealStash( - stash: GitStashCommit | { repoPath: string; ref: string }, + stash: GitStashCommit | { repoPath: string; ref: string; stashName: string }, options?: { select?: boolean; focus?: boolean; expand?: boolean | number; } ) { - const node = await this.findStashNode(stash); - if (node !== undefined) { - await this.reveal(node, options); - } + return window.withProgress( + { + location: ProgressLocation.Notification, + title: `Revealing stash '${stash.stashName}' in the Repositories view...`, + cancellable: true + }, + async (progress, token) => { + const node = await this.findStashNode(stash, token); + if (node !== undefined) { + await this.reveal(node, options); + } - return node; + return node; + } + ); } async revealStashes( @@ -202,17 +251,15 @@ export class RepositoriesView extends ViewBase { const node = await this.findNode(StashesNode.getId(repoPath), { maxDepth: 2, - getChildren: async n => { - // Only search for nodes in the same repo - if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) { - if (!n.id.startsWith(repoNodeId)) return emptyArray; - - if (!(n instanceof RepositoryNode)) { - return emptyArray; - } + canTraverse: n => { + // Only search for stashes nodes in the same repo + if (n instanceof RepositoriesNode) return true; + + if (n instanceof RepositoryNode) { + return n.id.startsWith(repoNodeId); } - return n.getChildren(); + return false; } }); diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index 7376a4a..85cfc82 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -1,5 +1,6 @@ 'use strict'; import { + CancellationToken, commands, ConfigurationChangeEvent, ConfigurationTarget, @@ -157,16 +158,18 @@ export abstract class ViewBase> implements TreeData id: string, options?: { allowPaging?: boolean; - getChildren?: (node: ViewNode) => ViewNode[] | Promise; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } ): Promise; async findNode( predicate: (node: ViewNode) => boolean, options?: { allowPaging?: boolean; - getChildren?: (node: ViewNode) => ViewNode[] | Promise; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } ): Promise; @log({ @@ -179,85 +182,150 @@ export abstract class ViewBase> implements TreeData predicate: string | ((node: ViewNode) => boolean), { allowPaging = false, - getChildren, - maxDepth = 2 + canTraverse, + maxDepth = 2, + token }: { allowPaging?: boolean; - getChildren?: (node: ViewNode) => ViewNode[] | Promise; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } = {} ): Promise { - if (getChildren === undefined) { - getChildren = n => n.getChildren(); - } - // If we have no root (e.g. never been initialized) force it so the tree will load properly if (this._root === undefined) { await this.show(); } - return this.findNodeCore( + // const node = await this.findNodeCoreDFS( + // typeof predicate === 'string' ? n => n.id === predicate : predicate, + // await this.ensureRoot().getChildren(), + // allowPaging, + // canTraverse, + // maxDepth + // ); + + const node = await this.findNodeCoreBFS( typeof predicate === 'string' ? n => n.id === predicate : predicate, - await getChildren(this.ensureRoot()), + this.ensureRoot(), allowPaging, - getChildren, - maxDepth + canTraverse, + maxDepth, + token ); + + return node; } - private async findNodeCore( + // private async findNodeCoreDFS( + // predicate: (node: ViewNode) => boolean, + // nodes: ViewNode[], + // allowPaging: boolean, + // canTraverse: ((node: ViewNode) => boolean) | undefined, + // depth: number + // ): Promise { + // if (depth === 0) return undefined; + + // const defaultPageSize = Container.config.advanced.maxListItems; + + // let child; + // let children; + // for (const node of nodes) { + // if (canTraverse !== undefined && !canTraverse(node)) continue; + + // children = await node.getChildren(); + // if (children.length === 0) continue; + + // child = children.find(predicate); + // if (child !== undefined) return child; + + // if (PageableViewNode.is(node)) { + // if (node.maxCount !== 0 && allowPaging) { + // let pageSize = defaultPageSize === 0 ? 0 : (node.maxCount || 0) + defaultPageSize; + // while (true) { + // await this.showMoreNodeChildren(node, pageSize); + + // child = (await node.getChildren()).find(predicate); + // if (child !== undefined) return child; + + // if (pageSize === 0) break; + + // pageSize = 0; + // } + // } + + // // Don't traverse into paged children + // continue; + // } + + // return this.findNodeCoreDFS(predicate, children, allowPaging, canTraverse, depth - 1); + // } + + // return undefined; + // } + + private async findNodeCoreBFS( predicate: (node: ViewNode) => boolean, - nodes: ViewNode[], + root: ViewNode, allowPaging: boolean, - getChildren: (node: ViewNode) => ViewNode[] | Promise, - depth: number + canTraverse: ((node: ViewNode) => boolean) | undefined, + maxDepth: number, + token: CancellationToken | undefined ): Promise { - const decendents: ViewNode[] = []; + const queue: (ViewNode | undefined)[] = [root, undefined]; const defaultPageSize = Container.config.advanced.maxListItems; - let child; - let children; - for (const node of nodes) { - children = await getChildren(node); - if (children.length === 0) continue; + let depth = 0; + let node: ViewNode | undefined; + let children: ViewNode[]; + while (queue.length > 1) { + if (token !== undefined && token.isCancellationRequested) return undefined; - child = children.find(predicate); - if (child !== undefined) return child; + node = queue.shift(); + if (node === undefined) { + depth++; - if (PageableViewNode.is(node)) { - // Don't descend into paged children - if (!allowPaging || node.maxCount === 0) continue; + queue.push(undefined); + if (depth > maxDepth) break; - if (defaultPageSize !== 0 && (node.maxCount === undefined || node.maxCount < defaultPageSize)) { - await this.showMoreNodeChildren(node, defaultPageSize); + continue; + } - children = await getChildren(node); - if (children.length === 0) continue; + if (predicate(node)) return node; + if (canTraverse !== undefined && !canTraverse(node)) continue; - child = children.find(predicate); - if (child !== undefined) return child; - } + children = await node.getChildren(); + if (children.length === 0) continue; - await this.showMoreNodeChildren(node, 0); + if (PageableViewNode.is(node)) { + let child = children.find(predicate); + if (child !== undefined) return child; - children = await getChildren(node); - if (children.length === 0) continue; + if (node.maxCount !== 0 && allowPaging) { + let pageSize = defaultPageSize === 0 ? 0 : (node.maxCount || 0) + defaultPageSize; + while (true) { + if (token !== undefined && token.isCancellationRequested) return undefined; - child = children.find(predicate); - if (child !== undefined) return child; + await this.showMoreNodeChildren(node, pageSize); + + child = (await node.getChildren()).find(predicate); + if (child !== undefined) return child; + + if (pageSize === 0) break; - // Don't descend into paged children + pageSize = 0; + } + } + + // Don't traverse into paged children continue; } - decendents.push(...children); + queue.push(...children); } - depth--; - if (depth === 0) return undefined; - - return this.findNodeCore(predicate, decendents, allowPaging, getChildren, depth); + return undefined; } @debug()