Skip to content

Commit e493b79

Browse files
Copilotalexr00
andauthored
Use custom InputBox UI for checkout-in-worktree, matching built-in Git: Create Worktree
Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/a494f95c-24ab-4aca-b78a-cd2ee4cca5a3 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 624f4a6 commit e493b79

1 file changed

Lines changed: 111 additions & 10 deletions

File tree

src/github/worktree.ts

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,21 @@ export async function checkoutPRInWorktree(
4444
// Prepare for operations
4545
const repoRootPath = repositoryToUse.rootUri.fsPath;
4646
const parentDir = path.dirname(repoRootPath);
47-
const defaultWorktreePath = path.join(parentDir, `pr-${pullRequestModel.number}`);
47+
const worktreeName = `pr-${pullRequestModel.number}`;
48+
// Match the default location convention used by VS Code's built-in `Git: Create Worktree...` command:
49+
// `<parentDir>/<repoBasename>.worktrees/<worktreeName>`.
50+
const defaultWorktreePath = path.join(parentDir, `${path.basename(repoRootPath)}.worktrees`, worktreeName);
4851
const branchName = prHead.ref;
4952
const remoteName = pullRequestModel.remote.remoteName;
5053

51-
// Ask user for worktree location first (not in progress)
52-
const worktreeUri = await vscode.window.showSaveDialog({
53-
defaultUri: vscode.Uri.file(defaultWorktreePath),
54-
title: vscode.l10n.t('Select Worktree Location'),
55-
saveLabel: vscode.l10n.t('Create Worktree'),
56-
});
57-
58-
if (!worktreeUri) {
54+
// Ask user for worktree location using a custom InputBox UI (matches the built-in
55+
// `Git: Create Worktree...` experience instead of showing the OS save dialog).
56+
const worktreePath = await promptForWorktreePath(repositoryToUse, worktreeName, defaultWorktreePath);
57+
if (!worktreePath) {
5958
return; // User cancelled
6059
}
6160

62-
const worktreePath = worktreeUri.fsPath;
61+
const worktreeUri = vscode.Uri.file(worktreePath);
6362
const trackedBranchName = `${remoteName}/${branchName}`;
6463

6564
try {
@@ -126,3 +125,105 @@ export async function checkoutPRInWorktree(
126125
vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage));
127126
}
128127
}
128+
129+
/**
130+
* Prompts the user for a worktree path using an `InputBox` that mirrors VS Code's
131+
* built-in `Git: Create Worktree...` UI: the path is pre-filled and editable, the
132+
* worktree-name segment is pre-selected, and an inline folder-picker button lets
133+
* the user browse to a parent directory.
134+
*
135+
* @param repository The repository the worktree will be created from (used to detect
136+
* conflicts with existing worktrees).
137+
* @param worktreeName The default leaf folder name for the new worktree (e.g. `pr-123`).
138+
* @param defaultWorktreePath The default full path to suggest in the input box.
139+
* @returns The chosen absolute path, or `undefined` if the user cancelled.
140+
*/
141+
async function promptForWorktreePath(
142+
repository: Repository,
143+
worktreeName: string,
144+
defaultWorktreePath: string
145+
): Promise<string | undefined> {
146+
const getValueSelection = (value: string): [number, number] | undefined => {
147+
if (!value || !worktreeName || !value.endsWith(worktreeName)) {
148+
return undefined;
149+
}
150+
const start = value.length - worktreeName.length;
151+
return [start, value.length];
152+
};
153+
154+
const getValidationMessage = (value: string): vscode.InputBoxValidationMessage | undefined => {
155+
const normalized = path.normalize(value);
156+
const conflict = repository.state.worktrees?.find(w => path.normalize(w.path) === normalized);
157+
return conflict ? {
158+
message: vscode.l10n.t('A worktree already exists at "{0}".', value),
159+
severity: vscode.InputBoxValidationSeverity.Warning
160+
} : undefined;
161+
};
162+
163+
const browseForParent = async (): Promise<string | undefined> => {
164+
const currentValue = inputBox.value;
165+
const defaultUri = currentValue
166+
? vscode.Uri.file(path.dirname(currentValue))
167+
: vscode.Uri.file(path.dirname(defaultWorktreePath));
168+
169+
const uris = await vscode.window.showOpenDialog({
170+
defaultUri,
171+
canSelectFiles: false,
172+
canSelectFolders: true,
173+
canSelectMany: false,
174+
openLabel: vscode.l10n.t('Select as Worktree Destination'),
175+
});
176+
177+
if (!uris || uris.length === 0) {
178+
return undefined;
179+
}
180+
return path.join(uris[0].fsPath, worktreeName);
181+
};
182+
183+
const disposables: vscode.Disposable[] = [];
184+
const inputBox = vscode.window.createInputBox();
185+
disposables.push(inputBox);
186+
187+
inputBox.title = vscode.l10n.t('Create Worktree');
188+
inputBox.placeholder = vscode.l10n.t('Worktree path');
189+
inputBox.prompt = vscode.l10n.t('Please provide a worktree path');
190+
inputBox.value = defaultWorktreePath;
191+
inputBox.valueSelection = getValueSelection(inputBox.value);
192+
inputBox.validationMessage = getValidationMessage(inputBox.value);
193+
inputBox.ignoreFocusOut = true;
194+
inputBox.buttons = [
195+
{
196+
iconPath: new vscode.ThemeIcon('folder'),
197+
tooltip: vscode.l10n.t('Select Worktree Destination'),
198+
location: vscode.QuickInputButtonLocation.Inline
199+
}
200+
];
201+
202+
try {
203+
inputBox.show();
204+
205+
return await new Promise<string | undefined>((resolve) => {
206+
disposables.push(inputBox.onDidHide(() => resolve(undefined)));
207+
disposables.push(inputBox.onDidAccept(() => {
208+
if (!inputBox.value) {
209+
return;
210+
}
211+
resolve(inputBox.value);
212+
inputBox.hide();
213+
}));
214+
disposables.push(inputBox.onDidChangeValue(value => {
215+
inputBox.validationMessage = getValidationMessage(value);
216+
}));
217+
disposables.push(inputBox.onDidTriggerButton(async () => {
218+
const chosen = await browseForParent();
219+
if (chosen) {
220+
inputBox.value = chosen;
221+
inputBox.valueSelection = getValueSelection(inputBox.value);
222+
inputBox.validationMessage = getValidationMessage(inputBox.value);
223+
}
224+
}));
225+
});
226+
} finally {
227+
disposables.forEach(d => d.dispose());
228+
}
229+
}

0 commit comments

Comments
 (0)