Skip to content

Commit 2fa7fe2

Browse files
committed
Improves the UX of the revision picker
Adds keyboard nav to revision picker
1 parent dea5488 commit 2fa7fe2

File tree

2 files changed

+148
-59
lines changed

2 files changed

+148
-59
lines changed

src/git/actions/commit.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -506,26 +506,33 @@ export async function openFileAtRevision(
506506

507507
let editor: TextEditor | undefined;
508508
try {
509-
editor = await findOrOpenEditor(uri, { throwOnError: true, ...opts }).catch(error => {
510-
if (error?.message?.includes('Unable to resolve nonexistent file')) {
511-
return showRevisionPicker(gitUri, {
512-
title: 'File not found in revision - pick another file to open instead',
513-
}).then(pickedUri => {
514-
return pickedUri ? findOrOpenEditor(pickedUri, opts) : undefined;
515-
});
516-
}
517-
throw error;
509+
editor = await findOrOpenEditor(uri, { throwOnError: true, ...opts });
510+
} catch (ex) {
511+
if (!ex?.message?.includes('Unable to resolve nonexistent file')) {
512+
void window.showErrorMessage(`Unable to open '${gitUri.relativePath}' in revision '${gitUri.sha}'`);
513+
return;
514+
}
515+
516+
const pickedUri = await showRevisionPicker(Container.instance, gitUri, {
517+
ignoreFocusOut: true,
518+
title: `Open File at Revision \u2022 Unable to open '${gitUri.relativePath}'`,
519+
placeholder: 'Choose a file revision to open',
520+
keyboard: {
521+
keys: ['right', 'alt+right', 'ctrl+right'],
522+
onDidPressKey: async (key, uri) => {
523+
await findOrOpenEditor(uri, { ...opts, preserveFocus: true, preview: true });
524+
},
525+
},
518526
});
527+
if (pickedUri == null) return;
519528

520-
if (annotationType != null && editor != null) {
521-
void (await Container.instance.fileAnnotations.show(editor, annotationType, {
522-
selection: { line: line },
523-
}));
524-
}
525-
} catch (error) {
526-
await window.showErrorMessage(
527-
`Unable to open '${gitUri.relativePath}' - file doesn't exist in selected revision`,
528-
);
529+
editor = await findOrOpenEditor(pickedUri, opts);
530+
}
531+
532+
if (annotationType != null && editor != null) {
533+
void (await Container.instance.fileAnnotations.show(editor, annotationType, {
534+
selection: { line: line },
535+
}));
529536
}
530537
}
531538

src/quickpicks/revisionPicker.ts

Lines changed: 123 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,140 @@
1-
// import path from "path";
2-
import type { Disposable, Uri } from "vscode";
3-
import { window } from "vscode";
4-
import { Container } from "../container";
5-
import type { GitUri } from "../git/gitUri";
6-
import { filterMap } from "../system/iterable";
7-
import { getQuickPickIgnoreFocusOut } from "../system/utils";
1+
import type { Disposable, Uri } from 'vscode';
2+
import { window } from 'vscode';
3+
import type { Keys } from '../constants';
4+
import type { Container } from '../container';
5+
import type { GitUri } from '../git/gitUri';
6+
import type { GitTreeEntry } from '../git/models/tree';
7+
import { filterMap } from '../system/iterable';
8+
import type { KeyboardScope } from '../system/keyboard';
9+
import { splitPath } from '../system/path';
10+
import { getQuickPickIgnoreFocusOut } from '../system/utils';
11+
import type { QuickPickItemOfT } from './items/common';
12+
13+
export type RevisionQuickPickItem = QuickPickItemOfT<GitTreeEntry>;
814

915
export async function showRevisionPicker(
16+
container: Container,
1017
uri: GitUri,
1118
options: {
12-
title: string;
19+
ignoreFocusOut?: boolean;
1320
initialPath?: string;
21+
keyboard?: {
22+
keys: Keys[];
23+
onDidPressKey(key: Keys, uri: Uri): void | Promise<void>;
24+
};
25+
placeholder?: string;
26+
title: string;
1427
},
1528
): Promise<Uri | undefined> {
1629
const disposables: Disposable[] = [];
30+
31+
const repoPath = uri.repoPath!;
32+
const ref = uri.sha!;
33+
34+
function getRevisionUri(item: RevisionQuickPickItem) {
35+
return container.git.getRevisionUri(ref, `${repoPath}/${item.item.path}`, repoPath);
36+
}
37+
1738
try {
18-
const picker = window.createQuickPick();
19-
picker.title = options.title;
20-
picker.value = options.initialPath ?? uri.relativePath;
21-
picker.placeholder = 'Enter path to file...';
22-
picker.matchOnDescription = true;
23-
picker.busy = true;
24-
picker.ignoreFocusOut = getQuickPickIgnoreFocusOut();
25-
26-
picker.show();
27-
28-
const tree = await Container.instance.git.getTreeForRevision(uri.repoPath, uri.sha!);
29-
picker.items = Array.from(filterMap(tree, file => {
30-
// Exclude directories
31-
if (file.type !== 'blob') { return null }
32-
return { label: file.path }
33-
// FIXME: Remove this unless we opt to show the directory in the description
34-
// const parsed = path.parse(file.path)
35-
// return { label: parsed.base, description: parsed.dir }
36-
}))
37-
picker.busy = false;
38-
39-
const pick = await new Promise<string | undefined>(resolve => {
39+
const quickpick = window.createQuickPick<RevisionQuickPickItem>();
40+
quickpick.ignoreFocusOut = options?.ignoreFocusOut ?? getQuickPickIgnoreFocusOut();
41+
42+
const value = options.initialPath ?? uri.relativePath;
43+
44+
let scope: KeyboardScope | undefined;
45+
if (options?.keyboard != null) {
46+
const { keyboard } = options;
47+
scope = container.keyboard.createScope(
48+
Object.fromEntries(
49+
keyboard.keys.map(key => [
50+
key,
51+
{
52+
onDidPressKey: async key => {
53+
if (quickpick.activeItems.length !== 0) {
54+
const [item] = quickpick.activeItems;
55+
if (item.item != null) {
56+
const ignoreFocusOut = quickpick.ignoreFocusOut;
57+
quickpick.ignoreFocusOut = true;
58+
59+
await keyboard.onDidPressKey(key, getRevisionUri(item));
60+
61+
quickpick.ignoreFocusOut = ignoreFocusOut;
62+
}
63+
}
64+
},
65+
},
66+
]),
67+
),
68+
);
69+
void scope.start();
70+
if (value != null) {
71+
void scope.pause(['left', 'ctrl+left', 'right', 'ctrl+right']);
72+
}
73+
disposables.push(scope);
74+
}
75+
76+
quickpick.title = options.title;
77+
quickpick.placeholder = options?.placeholder ?? 'Search files by name';
78+
quickpick.matchOnDescription = true;
79+
80+
quickpick.value = value;
81+
quickpick.busy = true;
82+
quickpick.show();
83+
84+
const tree = await container.git.getTreeForRevision(uri.repoPath, ref);
85+
const items: RevisionQuickPickItem[] = [
86+
...filterMap(tree, file => {
87+
// Exclude directories
88+
if (file.type !== 'blob') return undefined;
89+
90+
const [label, description] = splitPath(file.path, undefined, true);
91+
return {
92+
label: label,
93+
description: description === '.' ? '' : description,
94+
item: file,
95+
} satisfies RevisionQuickPickItem;
96+
}),
97+
];
98+
quickpick.items = items;
99+
quickpick.busy = false;
100+
101+
const pick = await new Promise<RevisionQuickPickItem | undefined>(resolve => {
40102
disposables.push(
41-
picker,
42-
picker.onDidHide(() => resolve(undefined)),
43-
picker.onDidAccept(() => {
44-
if (picker.activeItems.length === 0) return;
45-
resolve(picker.activeItems[0].label);
103+
quickpick,
104+
quickpick.onDidHide(() => resolve(undefined)),
105+
quickpick.onDidAccept(() => {
106+
if (quickpick.activeItems.length === 0) return;
107+
108+
resolve(quickpick.activeItems[0]);
109+
}),
110+
quickpick.onDidChangeValue(value => {
111+
if (scope == null) return;
112+
113+
// Pause the left/right keyboard commands if there is a value, otherwise the left/right arrows won't work in the input properly
114+
if (value.length !== 0) {
115+
void scope.pause(['left', 'ctrl+left', 'right', 'ctrl+right']);
116+
} else {
117+
void scope.resume();
118+
}
119+
120+
for (const item of items) {
121+
if (
122+
item.item.path.includes(value) &&
123+
!item.label.includes(value) &&
124+
!item.description!.includes(value)
125+
) {
126+
item.alwaysShow = true;
127+
} else {
128+
item.alwaysShow = false;
129+
}
130+
}
131+
quickpick.items = items;
46132
}),
47133
);
48134
});
49135

50-
return pick
51-
? Container.instance.git.getRevisionUri(uri.sha!, `${uri.repoPath}/${pick}`, uri.repoPath!)
52-
: undefined;
136+
return pick != null ? getRevisionUri(pick) : undefined;
53137
} finally {
54-
disposables.forEach(d => {
55-
d.dispose();
56-
});
138+
disposables.forEach(d => void d.dispose());
57139
}
58140
}

0 commit comments

Comments
 (0)