From 9ea07d38716dd0871526e1786d393ec5003bbe5e Mon Sep 17 00:00:00 2001 From: Victor Hallberg Date: Thu, 20 Jul 2023 19:49:40 +0200 Subject: [PATCH] Fixes #708 - Prompt for file path when missing from revision Improves the `openFileAtRevision()` function by: - Asking user to enter another path when the requested file doesn't exist in the selected revision. - Showing an error message on subsequent failure (or if an error is thrown). The above function is used by a number of commands: - `gitlens.openFileRevision` - `gitlens.openFileRevisionFrom` - `gitlens.openRevisionFile` Also renames the `rethrow` argument of `utils.openEditor()` to `throwOnError` to align it with `utils.findOrOpenEditor()`. --- CHANGELOG.md | 4 ++ src/commands/openFileFromRemote.ts | 2 +- src/git/actions/commit.ts | 71 ++++++++++++++++++++++++++---- src/system/utils.ts | 6 +-- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3350cf6f78cd..6cd8ad653f7a5 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..._ commands by showing a quickpicker prompt if the requested file doesn't exist in the selected revision — thanks to [PR #2825](https://github.com/gitkraken/vscode-gitlens/pull/2825) by Victor Hallberg ([@mogelbrod](https://github.com/mogelbrod)) + ### Changed - Changes _Compact Graph Column Layout_ context menu command to _Use Compact Graph Column_ for better clarity diff --git a/src/commands/openFileFromRemote.ts b/src/commands/openFileFromRemote.ts index b5d62386442c6..eb17a9cde5365 100644 --- a/src/commands/openFileFromRemote.ts +++ b/src/commands/openFileFromRemote.ts @@ -54,7 +54,7 @@ export class OpenFileFromRemoteCommand extends Command { } try { - await openEditor(local.uri, { selection: selection, rethrow: true }); + await openEditor(local.uri, { selection: selection, throwOnError: true }); } catch { const uris = await window.showOpenDialog({ title: 'Open local file', diff --git a/src/git/actions/commit.ts b/src/git/actions/commit.ts index 65c83fcbedf07..34ffecffca33f 100644 --- a/src/git/actions/commit.ts +++ b/src/git/actions/commit.ts @@ -1,5 +1,6 @@ -import type { TextDocumentShowOptions } from 'vscode'; +import type { QuickPickItem, TextDocumentShowOptions, TextEditor } from 'vscode'; import { env, Range, Uri, window } from 'vscode'; +import type { Disposable } from '../../api/gitlens'; import type { DiffWithCommandArgs, DiffWithPreviousCommandArgs, @@ -14,7 +15,7 @@ import { Commands } from '../../constants'; import { Container } from '../../container'; import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/protocol'; import { executeCommand, executeEditorCommand } from '../../system/command'; -import { findOrOpenEditor, findOrOpenEditors } from '../../system/utils'; +import { findOrOpenEditor, findOrOpenEditors, getQuickPickIgnoreFocusOut } from '../../system/utils'; import { GitUri } from '../gitUri'; import type { GitCommit } from '../models/commit'; import { isCommit } from '../models/commit'; @@ -389,7 +390,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'); @@ -427,11 +428,65 @@ 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 pickFileAtRevision(gitUri, gitUri.relativePath).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`, + ); + } +} + +async function pickFileAtRevision(uri: GitUri, initialPath?: string): Promise { + const disposables: Disposable[] = []; + try { + const picker = window.createQuickPick(); + Object.assign(picker, { + title: 'File not found in revision - pick another file to open instead', + placeholder: 'Enter path to file...', + value: initialPath, + busy: true, + ignoreFocusOut: getQuickPickIgnoreFocusOut(), + }); + picker.show(); + + const tree = await Container.instance.git.getTreeForRevision(uri.repoPath, uri.sha!); + picker.items = tree.map((file): QuickPickItem => ({ label: file.path })); + 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); + }), + ); + }); + + const result = pick ? uri.with({ path: `${uri.repoPath!}/${pick}` }) : undefined; + return result; + } finally { + disposables.forEach(d => { + d.dispose(); + }); } } diff --git a/src/system/utils.ts b/src/system/utils.ts index f595fa8490c5c..5692f4764f0e5 100644 --- a/src/system/utils.ts +++ b/src/system/utils.ts @@ -106,9 +106,9 @@ export function isTextEditor(editor: TextEditor): boolean { export async function openEditor( uri: Uri, - options: TextDocumentShowOptions & { rethrow?: boolean } = {}, + options: TextDocumentShowOptions & { throwOnError?: boolean } = {}, ): Promise { - const { rethrow, ...opts } = options; + const { throwOnError, ...opts } = options; try { if (isGitUri(uri)) { uri = uri.documentUri(); @@ -135,7 +135,7 @@ export async function openEditor( return undefined; } - if (rethrow) throw ex; + if (throwOnError) throw ex; Logger.error(ex, 'openEditor'); return undefined;