diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a30f6bba2d01..b8806640a1299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- Adds support for opening renamed/deleted files using the _Open File at Revision..._ & _Open File at Revision from..._ commands by showing a quick pick menu if the requested file doesn't exist in the selected revision — closes [#708](https://github.com/gitkraken/vscode-gitlens/issues/708) thanks to [PR #2825](https://github.com/gitkraken/vscode-gitlens/pull/2825) by Victor Hallberg ([@mogelbrod](https://github.com/mogelbrod)) + ### Changed - Changes blame to show the last modified time of the file for uncommitted changes diff --git a/src/git/actions/commit.ts b/src/git/actions/commit.ts index 6421cf20555e1..083fe12a2a5b1 100644 --- a/src/git/actions/commit.ts +++ b/src/git/actions/commit.ts @@ -1,4 +1,4 @@ -import type { TextDocumentShowOptions } from 'vscode'; +import type { TextDocumentShowOptions, TextEditor } from 'vscode'; import { env, Range, Uri, window, workspace } from 'vscode'; import type { DiffWithCommandArgs } from '../../commands/diffWith'; import type { DiffWithPreviousCommandArgs } from '../../commands/diffWithPrevious'; @@ -12,6 +12,7 @@ import type { FileAnnotationType } from '../../config'; import { Commands, GlyphChars } from '../../constants'; import { Container } from '../../container'; import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/protocol'; +import { showRevisionPicker } from '../../quickpicks/revisionPicker'; import { executeCommand, executeEditorCommand } from '../../system/command'; import { configuration } from '../../system/configuration'; import { findOrOpenEditor, findOrOpenEditors, openChangesEditor } from '../../system/utils'; @@ -463,7 +464,7 @@ export async function openFileAtRevision( commitOrOptions?: GitCommit | TextDocumentShowOptions, options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number }, ): Promise { - let uri; + let uri: Uri; if (fileOrRevisionUri instanceof Uri) { if (isCommit(commitOrOptions)) throw new Error('Invalid arguments'); @@ -501,11 +502,30 @@ export async function openFileAtRevision( opts.selection = new Range(line, 0, line, 0); } - const editor = await findOrOpenEditor(uri, opts); - if (annotationType != null && editor != null) { - void (await Container.instance.fileAnnotations.show(editor, annotationType, { - selection: { line: line }, - })); + const gitUri = await GitUri.fromUri(uri); + + let editor: TextEditor | undefined; + try { + editor = await findOrOpenEditor(uri, { throwOnError: true, ...opts }).catch(error => { + if (error?.message?.includes('Unable to resolve nonexistent file')) { + return showRevisionPicker(gitUri, { + title: 'File not found in revision - pick another file to open instead', + }).then(pickedUri => { + return pickedUri ? findOrOpenEditor(pickedUri, opts) : undefined; + }); + } + throw error; + }); + + if (annotationType != null && editor != null) { + void (await Container.instance.fileAnnotations.show(editor, annotationType, { + selection: { line: line }, + })); + } + } catch (error) { + await window.showErrorMessage( + `Unable to open '${gitUri.relativePath}' - file doesn't exist in selected revision`, + ); } } diff --git a/src/quickpicks/revisionPicker.ts b/src/quickpicks/revisionPicker.ts new file mode 100644 index 0000000000000..1d2b2678eac32 --- /dev/null +++ b/src/quickpicks/revisionPicker.ts @@ -0,0 +1,58 @@ +// import path from "path"; +import type { Disposable, Uri } from "vscode"; +import { window } from "vscode"; +import { Container } from "../container"; +import type { GitUri } from "../git/gitUri"; +import { filterMap } from "../system/iterable"; +import { getQuickPickIgnoreFocusOut } from "../system/utils"; + +export async function showRevisionPicker( + uri: GitUri, + options: { + title: string; + initialPath?: string; + }, +): Promise { + const disposables: Disposable[] = []; + try { + const picker = window.createQuickPick(); + picker.title = options.title; + picker.value = options.initialPath ?? uri.relativePath; + picker.placeholder = 'Enter path to file...'; + picker.matchOnDescription = true; + picker.busy = true; + picker.ignoreFocusOut = getQuickPickIgnoreFocusOut(); + + picker.show(); + + const tree = await Container.instance.git.getTreeForRevision(uri.repoPath, uri.sha!); + picker.items = Array.from(filterMap(tree, file => { + // Exclude directories + if (file.type !== 'blob') { return null } + return { label: file.path } + // FIXME: Remove this unless we opt to show the directory in the description + // const parsed = path.parse(file.path) + // return { label: parsed.base, description: parsed.dir } + })) + picker.busy = false; + + const pick = await new Promise(resolve => { + disposables.push( + picker, + picker.onDidHide(() => resolve(undefined)), + picker.onDidAccept(() => { + if (picker.activeItems.length === 0) return; + resolve(picker.activeItems[0].label); + }), + ); + }); + + return pick + ? Container.instance.git.getRevisionUri(uri.sha!, `${uri.repoPath}/${pick}`, uri.repoPath!) + : undefined; + } finally { + disposables.forEach(d => { + d.dispose(); + }); + } +}