Improve Zed prompts for file path selection (#32014)

Part of https://github.com/zed-industries/zed/discussions/31653
`"use_system_path_prompts": false` is needed in settings for these to
appear as modals for new file save and file open.

Fixed a very subpar experience of the "save new file" Zed modal,
compared to a similar "open file path" Zed modal by uniting their code.

Before:


https://github.com/user-attachments/assets/c4082b70-6cdc-4598-a416-d491011c8ac4


After:



https://github.com/user-attachments/assets/21ca672a-ae40-426c-b68f-9efee4f93c8c


Also 

* alters both prompts to start in the current worktree directory, with
the fallback to home directory.
* adjusts the code to handle Windows paths better

Release Notes:

- Improved Zed prompts for file path selection

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Kirill Bulatov 2025-06-03 23:35:25 +03:00 committed by GitHub
parent 8c46a4f594
commit 4aabba6cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 721 additions and 792 deletions

View file

@ -899,9 +899,10 @@ pub enum OpenVisible {
type PromptForNewPath = Box<
dyn Fn(
&mut Workspace,
DirectoryLister,
&mut Window,
&mut Context<Workspace>,
) -> oneshot::Receiver<Option<ProjectPath>>,
) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
>;
type PromptForOpenPath = Box<
@ -1874,25 +1875,25 @@ impl Workspace {
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_paths(path_prompt_options);
cx.spawn_in(window, async move |this, cx| {
cx.spawn_in(window, async move |workspace, cx| {
let Ok(result) = abs_path.await else {
return Ok(());
};
match result {
Ok(result) => {
tx.send(result).log_err();
tx.send(result).ok();
}
Err(err) => {
let rx = this.update_in(cx, |this, window, cx| {
this.show_portal_error(err.to_string(), cx);
let prompt = this.on_prompt_for_open_path.take().unwrap();
let rx = prompt(this, lister, window, cx);
this.on_prompt_for_open_path = Some(prompt);
let rx = workspace.update_in(cx, |workspace, window, cx| {
workspace.show_portal_error(err.to_string(), cx);
let prompt = workspace.on_prompt_for_open_path.take().unwrap();
let rx = prompt(workspace, lister, window, cx);
workspace.on_prompt_for_open_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).log_err();
tx.send(path).ok();
}
}
};
@ -1906,77 +1907,58 @@ impl Workspace {
pub fn prompt_for_new_path(
&mut self,
lister: DirectoryLister,
window: &mut Window,
cx: &mut Context<Self>,
) -> oneshot::Receiver<Option<ProjectPath>> {
if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
if self.project.read(cx).is_via_collab()
|| self.project.read(cx).is_via_ssh()
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
{
let prompt = self.on_prompt_for_new_path.take().unwrap();
let rx = prompt(self, window, cx);
let rx = prompt(self, lister, window, cx);
self.on_prompt_for_new_path = Some(prompt);
return rx;
}
let (tx, rx) = oneshot::channel();
cx.spawn_in(window, async move |this, cx| {
let abs_path = this.update(cx, |this, cx| {
let mut relative_to = this
cx.spawn_in(window, async move |workspace, cx| {
let abs_path = workspace.update(cx, |workspace, cx| {
let relative_to = workspace
.most_recent_active_path(cx)
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
if relative_to.is_none() {
let project = this.project.read(cx);
relative_to = project
.visible_worktrees(cx)
.filter_map(|worktree| {
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.or_else(|| {
let project = workspace.project.read(cx);
project.visible_worktrees(cx).find_map(|worktree| {
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})
.next()
};
cx.prompt_for_new_path(&relative_to.unwrap_or_else(|| PathBuf::from("")))
})
.or_else(std::env::home_dir)
.unwrap_or_else(|| PathBuf::from(""));
cx.prompt_for_new_path(&relative_to)
})?;
let abs_path = match abs_path.await? {
Ok(path) => path,
Err(err) => {
let rx = this.update_in(cx, |this, window, cx| {
this.show_portal_error(err.to_string(), cx);
let rx = workspace.update_in(cx, |workspace, window, cx| {
workspace.show_portal_error(err.to_string(), cx);
let prompt = this.on_prompt_for_new_path.take().unwrap();
let rx = prompt(this, window, cx);
this.on_prompt_for_new_path = Some(prompt);
let prompt = workspace.on_prompt_for_new_path.take().unwrap();
let rx = prompt(workspace, lister, window, cx);
workspace.on_prompt_for_new_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).log_err();
tx.send(path).ok();
}
return anyhow::Ok(());
}
};
let project_path = abs_path.and_then(|abs_path| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.find_or_create_worktree(abs_path, true, cx)
})
})
.ok()
});
if let Some(project_path) = project_path {
let (worktree, path) = project_path.await?;
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
tx.send(Some(ProjectPath {
worktree_id,
path: path.into(),
}))
.ok();
} else {
tx.send(None).ok();
}
tx.send(abs_path.map(|path| vec![path])).ok();
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach();
rx
}