From abc5abcd8baaaffd442ec746b35b53ca84bdf79b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Jul 2024 17:04:15 -0600 Subject: [PATCH] open picker (#14524) Release Notes: - linux: Added a fallback Open picker for when XDG is not working - Added a new setting `use_system_path_prompts` (default true) that can be disabled to use Zed's builtin keyboard-driven prompts. --------- Co-authored-by: Max --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 8 +- assets/keymaps/default-macos.json | 16 +- assets/settings/default.json | 3 + crates/file_finder/src/file_finder.rs | 3 + crates/file_finder/src/new_path_prompt.rs | 14 +- crates/file_finder/src/open_path_prompt.rs | 293 +++++++++++++++++++++ crates/menu/src/menu.rs | 1 - crates/picker/src/picker.rs | 15 +- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 18 ++ crates/tasks_ui/src/modal.rs | 12 +- crates/workspace/src/workspace.rs | 131 ++++++--- crates/workspace/src/workspace_settings.rs | 6 + 14 files changed, 453 insertions(+), 69 deletions(-) create mode 100644 crates/file_finder/src/open_path_prompt.rs diff --git a/Cargo.lock b/Cargo.lock index 123e5fc9d7..dc78730aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8077,6 +8077,7 @@ dependencies = [ "serde_json", "settings", "sha2 0.10.7", + "shellexpand 2.1.2", "shlex", "similar", "smol", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 11b8b5bd30..c2ea8652a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -19,7 +19,6 @@ "escape": "menu::Cancel", "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "shift-enter": "picker::UseSelectedQuery", "alt-enter": ["picker::ConfirmInput", { "secondary": false }], "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], "ctrl-shift-w": "workspace::CloseWindow", @@ -567,6 +566,13 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "Picker > Editor", + "bindings": { + "tab": "picker::ConfirmCompletion", + "alt-enter": ["picker::ConfirmInput", { "secondary": false }] + } + }, { "context": "ChannelModal > Picker > Editor", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ba6a41ea6b..eda162af64 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -594,6 +594,14 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "Picker > Editor", + "bindings": { + "tab": "picker::ConfirmCompletion", + "alt-enter": ["picker::ConfirmInput", { "secondary": false }], + "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] + } + }, { "context": "ChannelModal > Picker > Editor", "bindings": { @@ -613,14 +621,6 @@ "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, - { - "context": "Picker", - "bindings": { - "f2": "picker::UseSelectedQuery", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] - } - }, { "context": "Terminal", "bindings": { diff --git a/assets/settings/default.json b/assets/settings/default.json index 4a1bcfc0ed..c5b3b8b27e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -94,6 +94,9 @@ // 3. Never close the window // "when_closing_with_no_tabs": "keep_window_open", "when_closing_with_no_tabs": "platform_default", + // Whether to use the system provided dialogs for Open and Save As. + // When set to false, Zed will use the built-in keyboard-first pickers. + "use_system_path_prompts": true, // Whether the cursor blinks in the editor. "cursor_blink": true, // How to highlight the current line in the editor. diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3161825aff..2165fa9152 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -2,6 +2,7 @@ mod file_finder_tests; mod new_path_prompt; +mod open_path_prompt; use collections::{BTreeSet, HashMap}; use editor::{scroll::Autoscroll, Bias, Editor}; @@ -13,6 +14,7 @@ use gpui::{ }; use itertools::Itertools; use new_path_prompt::NewPathPrompt; +use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use settings::Settings; @@ -41,6 +43,7 @@ pub struct FileFinder { pub fn init(cx: &mut AppContext) { cx.observe_new_views(FileFinder::register).detach(); cx.observe_new_views(NewPathPrompt::register).detach(); + cx.observe_new_views(OpenPathPrompt::register).detach(); } impl FileFinder { diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs index 5de1b7b648..a6fa38b7b0 100644 --- a/crates/file_finder/src/new_path_prompt.rs +++ b/crates/file_finder/src/new_path_prompt.rs @@ -197,14 +197,12 @@ pub struct NewPathDelegate { } impl NewPathPrompt { - pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext) { - if workspace.project().read(cx).is_remote() { - workspace.set_prompt_for_new_path(Box::new(|workspace, cx| { - let (tx, rx) = futures::channel::oneshot::channel(); - Self::prompt_for_new_path(workspace, tx, cx); - rx - })); - } + pub(crate) fn register(workspace: &mut Workspace, _cx: &mut ViewContext) { + workspace.set_prompt_for_new_path(Box::new(|workspace, cx| { + let (tx, rx) = futures::channel::oneshot::channel(); + Self::prompt_for_new_path(workspace, tx, cx); + rx + })); } fn prompt_for_new_path( diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs new file mode 100644 index 0000000000..75557c7fc5 --- /dev/null +++ b/crates/file_finder/src/open_path_prompt.rs @@ -0,0 +1,293 @@ +use futures::channel::oneshot; +use fuzzy::StringMatchCandidate; +use gpui::Model; +use picker::{Picker, PickerDelegate}; +use project::{compare_paths, Project}; +use std::{ + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; +use ui::{prelude::*, LabelLike, ListItemSpacing}; +use ui::{ListItem, ViewContext}; +use util::maybe; +use workspace::Workspace; + +pub(crate) struct OpenPathPrompt; + +pub struct OpenPathDelegate { + tx: Option>>>, + project: Model, + selected_index: usize, + directory_state: Option, + matches: Vec, + cancel_flag: Arc, + should_dismiss: bool, +} + +struct DirectoryState { + path: String, + match_candidates: Vec, + error: Option, +} + +impl OpenPathPrompt { + pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.set_prompt_for_open_path(Box::new(|workspace, cx| { + let (tx, rx) = futures::channel::oneshot::channel(); + Self::prompt_for_open_path(workspace, tx, cx); + rx + })); + } + + fn prompt_for_open_path( + workspace: &mut Workspace, + tx: oneshot::Sender>>, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + workspace.toggle_modal(cx, |cx| { + let delegate = OpenPathDelegate { + tx: Some(tx), + project: project.clone(), + selected_index: 0, + directory_state: None, + matches: Vec::new(), + cancel_flag: Arc::new(AtomicBool::new(false)), + should_dismiss: true, + }; + + let picker = Picker::uniform_list(delegate, cx).width(rems(34.)); + let query = if let Some(worktree) = project.read(cx).visible_worktrees(cx).next() { + worktree.read(cx).abs_path().to_string_lossy().to_string() + } else { + "~/".to_string() + }; + picker.set_query(query, cx); + picker + }); + } +} + +impl PickerDelegate for OpenPathDelegate { + type ListItem = ui::ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = ix; + cx.notify(); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let project = self.project.clone(); + let (mut dir, suffix) = if let Some(index) = query.rfind('/') { + (query[..index].to_string(), query[index + 1..].to_string()) + } else { + (query, String::new()) + }; + if dir == "" { + dir = "/".to_string(); + } + + let query = if self + .directory_state + .as_ref() + .map_or(false, |s| s.path == dir) + { + None + } else { + Some(project.update(cx, |project, cx| { + project.completions_for_open_path_query(dir.clone(), cx) + })) + }; + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); + + cx.spawn(|this, mut cx| async move { + if let Some(query) = query { + let paths = query.await; + if cancel_flag.load(atomic::Ordering::Relaxed) { + return; + } + + this.update(&mut cx, |this, _| { + this.delegate.directory_state = Some(match paths { + Ok(mut paths) => { + paths.sort_by(|a, b| { + compare_paths( + (a.strip_prefix(&dir).unwrap_or(Path::new("")), true), + (b.strip_prefix(&dir).unwrap_or(Path::new("")), true), + ) + }); + let match_candidates = paths + .iter() + .enumerate() + .filter_map(|(ix, path)| { + Some(StringMatchCandidate::new( + ix, + path.file_name()?.to_string_lossy().into(), + )) + }) + .collect::>(); + + DirectoryState { + match_candidates, + path: dir, + error: None, + } + } + Err(err) => DirectoryState { + match_candidates: vec![], + path: dir, + error: Some(err.to_string().into()), + }, + }); + }) + .ok(); + } + + let match_candidates = this + .update(&mut cx, |this, cx| { + let directory_state = this.delegate.directory_state.as_ref()?; + if directory_state.error.is_some() { + this.delegate.matches.clear(); + this.delegate.selected_index = 0; + cx.notify(); + return None; + } + + Some(directory_state.match_candidates.clone()) + }) + .unwrap_or(None); + + let Some(mut match_candidates) = match_candidates else { + return; + }; + + if !suffix.starts_with('.') { + match_candidates.retain(|m| !m.string.starts_with('.')); + } + + if suffix == "" { + this.update(&mut cx, |this, cx| { + this.delegate.matches.clear(); + this.delegate + .matches + .extend(match_candidates.iter().map(|m| m.id)); + + cx.notify(); + }) + .ok(); + return; + } + + let matches = fuzzy::match_strings( + &match_candidates.as_slice(), + &suffix, + false, + 100, + &cancel_flag, + cx.background_executor().clone(), + ) + .await; + if cancel_flag.load(atomic::Ordering::Relaxed) { + return; + } + + this.update(&mut cx, |this, cx| { + this.delegate.matches.clear(); + this.delegate + .matches + .extend(matches.into_iter().map(|m| m.candidate_id)); + this.delegate.matches.sort(); + cx.notify(); + }) + .ok(); + }) + } + + fn confirm_completion(&self, query: String) -> Option { + Some( + maybe!({ + let m = self.matches.get(self.selected_index)?; + let directory_state = self.directory_state.as_ref()?; + let candidate = directory_state.match_candidates.get(*m)?; + Some(format!("{}/{}", directory_state.path, candidate.string)) + }) + .unwrap_or(query), + ) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + let Some(m) = self.matches.get(self.selected_index) else { + return; + }; + let Some(directory_state) = self.directory_state.as_ref() else { + return; + }; + let Some(candidate) = directory_state.match_candidates.get(*m) else { + return; + }; + let result = Path::new(&directory_state.path).join(&candidate.string); + if let Some(tx) = self.tx.take() { + tx.send(Some(vec![result])).ok(); + } + cx.emit(gpui::DismissEvent); + } + + fn should_dismiss(&self) -> bool { + self.should_dismiss + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + if let Some(tx) = self.tx.take() { + tx.send(None).ok(); + } + cx.emit(gpui::DismissEvent) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let m = self.matches.get(ix)?; + let directory_state = self.directory_state.as_ref()?; + let candidate = directory_state.match_candidates.get(*m)?; + + Some( + ListItem::new(ix) + .spacing(ListItemSpacing::Sparse) + .inset(true) + .selected(selected) + .child(LabelLike::new().child(candidate.string.clone())), + ) + } + + fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString { + if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) { + error + } else { + "No such file or directory".into() + } + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + Arc::from("[directory/]filename.ext") + } +} diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs index 14d8d0624f..0818a6e6ff 100644 --- a/crates/menu/src/menu.rs +++ b/crates/menu/src/menu.rs @@ -19,6 +19,5 @@ actions!( SelectNext, SelectFirst, SelectLast, - UseSelectedQuery, ] ); diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 18618caa4a..4c10220f37 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -20,7 +20,7 @@ enum ElementContainer { UniformList(UniformListScrollHandle), } -actions!(picker, [UseSelectedQuery]); +actions!(picker, [ConfirmCompletion]); /// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally, /// performing some kind of action on it. @@ -87,10 +87,10 @@ pub trait PickerDelegate: Sized + 'static { false } + /// Override if you want to have update the query instead of confirming. fn confirm_update_query(&mut self, _cx: &mut ViewContext>) -> Option { None } - fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); /// Instead of interacting with currently selected entry, treats editor input literally, /// performing some kind of action on it. @@ -99,7 +99,7 @@ pub trait PickerDelegate: Sized + 'static { fn should_dismiss(&self) -> bool { true } - fn selected_as_query(&self) -> Option { + fn confirm_completion(&self, _query: String) -> Option { None } @@ -349,10 +349,11 @@ impl Picker { self.delegate.confirm_input(input.secondary, cx); } - fn use_selected_query(&mut self, _: &UseSelectedQuery, cx: &mut ViewContext) { - if let Some(new_query) = self.delegate.selected_as_query() { + fn confirm_completion(&mut self, _: &ConfirmCompletion, cx: &mut ViewContext) { + if let Some(new_query) = self.delegate.confirm_completion(self.query(cx)) { self.set_query(new_query, cx); - cx.stop_propagation(); + } else { + cx.propagate() } } @@ -571,7 +572,7 @@ impl Render for Picker { .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::secondary_confirm)) - .on_action(cx.listener(Self::use_selected_query)) + .on_action(cx.listener(Self::confirm_completion)) .on_action(cx.listener(Self::confirm_input)) .child(match &self.head { Head::Editor(editor) => self.delegate.render_editor(&editor.clone(), cx), diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index af59dee0e2..5774020b2d 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -59,6 +59,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true sha2.workspace = true +shellexpand.workspace = true shlex.workspace = true similar = "1.3" smol.workspace = true diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 67170745e7..1158fd4f09 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7559,6 +7559,24 @@ impl Project { } } + pub fn completions_for_open_path_query( + &self, + query: String, + cx: &mut ModelContext, + ) -> Task>> { + let fs = self.fs.clone(); + cx.background_executor().spawn(async move { + let mut results = vec![]; + let expanded = shellexpand::tilde(&query); + let query = Path::new(expanded.as_ref()); + let mut response = fs.read_dir(query).await?; + while let Some(path) = response.next().await { + results.push(path?); + } + Ok(results) + }) + } + fn create_local_worktree( &mut self, abs_path: impl AsRef, diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 34c1c127d6..f13beb4b05 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -462,7 +462,7 @@ impl PickerDelegate for TasksModalDelegate { ) } - fn selected_as_query(&self) -> Option { + fn confirm_completion(&self, _: String) -> Option { let task_index = self.matches.get(self.selected_index())?.candidate_id; let tasks = self.candidates.as_ref()?; let (_, task) = tasks.get(task_index)?; @@ -491,11 +491,7 @@ impl PickerDelegate for TasksModalDelegate { fn render_footer(&self, cx: &mut ViewContext>) -> Option { let is_recent_selected = self.divider_index >= Some(self.selected_index); let current_modifiers = cx.modifiers(); - let left_button = if is_recent_selected { - Some(("Edit task", picker::UseSelectedQuery.boxed_clone())) - } else if !self.matches.is_empty() { - Some(("Edit template", picker::UseSelectedQuery.boxed_clone())) - } else if self + let left_button = if self .project .read(cx) .task_inventory() @@ -663,7 +659,7 @@ mod tests { "Only one task should match the query {query_str}" ); - cx.dispatch_action(picker::UseSelectedQuery); + cx.dispatch_action(picker::ConfirmCompletion); assert_eq!( query(&tasks_picker, cx), "echo 4", @@ -710,7 +706,7 @@ mod tests { "Last recently used one show task should be listed first" ); - cx.dispatch_action(picker::UseSelectedQuery); + cx.dispatch_action(picker::ConfirmCompletion); assert_eq!( query(&tasks_picker, cx), query_str, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c3c230bd9a..3a2529f80d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -604,6 +604,10 @@ type PromptForNewPath = Box< dyn Fn(&mut Workspace, &mut ViewContext) -> oneshot::Receiver>, >; +type PromptForOpenPath = Box< + dyn Fn(&mut Workspace, &mut ViewContext) -> oneshot::Receiver>>, +>; + /// Collects everything project-related for a certain window opened. /// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`. /// @@ -646,6 +650,7 @@ pub struct Workspace { centered_layout: bool, bounds_save_task_queued: Option>, on_prompt_for_new_path: Option, + on_prompt_for_open_path: Option, render_disconnected_overlay: Option) -> AnyElement>>, } @@ -931,6 +936,7 @@ impl Workspace { centered_layout: false, bounds_save_task_queued: None, on_prompt_for_new_path: None, + on_prompt_for_open_path: None, render_disconnected_overlay: None, } } @@ -1312,6 +1318,10 @@ impl Workspace { self.on_prompt_for_new_path = Some(prompt) } + pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) { + self.on_prompt_for_open_path = Some(prompt) + } + pub fn set_render_disconnected_overlay( &mut self, render: impl Fn(&mut Self, &mut ViewContext) -> AnyElement + 'static, @@ -1319,11 +1329,60 @@ impl Workspace { self.render_disconnected_overlay = Some(Box::new(render)) } + pub fn prompt_for_open_path( + &mut self, + path_prompt_options: PathPromptOptions, + cx: &mut ViewContext, + ) -> oneshot::Receiver>> { + if self.project.read(cx).is_remote() + || !WorkspaceSettings::get_global(cx).use_system_path_prompts + { + let prompt = self.on_prompt_for_open_path.take().unwrap(); + let rx = prompt(self, cx); + self.on_prompt_for_open_path = Some(prompt); + rx + } else { + let (tx, rx) = oneshot::channel(); + let abs_path = cx.prompt_for_paths(path_prompt_options); + + cx.spawn(|this, mut cx| async move { + let Ok(result) = abs_path.await else { + return Ok(()); + }; + + match result { + Ok(result) => { + tx.send(result).log_err(); + } + Err(err) => { + let rx = this.update(&mut cx, |this, cx| { + this.show_portal_error(err.to_string(), cx); + let prompt = this.on_prompt_for_open_path.take().unwrap(); + let rx = prompt(this, cx); + this.on_prompt_for_open_path = Some(prompt); + rx + })?; + if let Ok(path) = rx.await { + tx.send(path).log_err(); + } + } + }; + anyhow::Ok(()) + }) + .detach(); + + rx + } + } + pub fn prompt_for_new_path( &mut self, cx: &mut ViewContext, ) -> oneshot::Receiver> { - if let Some(prompt) = self.on_prompt_for_new_path.take() { + if self.project.read(cx).is_remote() + || !WorkspaceSettings::get_global(cx).use_system_path_prompts + { + let prompt = self.on_prompt_for_new_path.take().unwrap(); let rx = prompt(self, cx); self.on_prompt_for_new_path = Some(prompt); rx @@ -1339,14 +1398,23 @@ impl Workspace { let (tx, rx) = oneshot::channel(); let abs_path = cx.prompt_for_new_path(&start_abs_path); cx.spawn(|this, mut cx| async move { - let abs_path: Option = - Flatten::flatten(abs_path.await.map_err(|e| e.into())).map_err(|err| { - this.update(&mut cx, |this, cx| { + let abs_path = match abs_path.await? { + Ok(path) => path, + Err(err) => { + let rx = this.update(&mut cx, |this, cx| { this.show_portal_error(err.to_string(), cx); - }) - .ok(); - err - })?; + + let prompt = this.on_prompt_for_new_path.take().unwrap(); + let rx = prompt(this, cx); + this.on_prompt_for_new_path = Some(prompt); + rx + })?; + if let Ok(path) = rx.await { + tx.send(path).log_err(); + } + return anyhow::Ok(()); + } + }; let project_path = abs_path.and_then(|abs_path| { this.update(&mut cx, |this, cx| { @@ -1629,23 +1697,18 @@ impl Workspace { self.client() .telemetry() .report_app_event("open project".to_string()); - let paths = cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: true, - multiple: true, - }); + let paths = self.prompt_for_open_path( + PathPromptOptions { + files: true, + directories: true, + multiple: true, + }, + cx, + ); cx.spawn(|this, mut cx| async move { - let paths = match Flatten::flatten(paths.await.map_err(|e| e.into())) { - Ok(Some(paths)) => paths, - Ok(None) => return, - Err(err) => { - this.update(&mut cx, |this, cx| { - this.show_portal_error(err.to_string(), cx); - }) - .ok(); - return; - } + let Some(paths) = paths.await.log_err().flatten() else { + return; }; if let Some(task) = this @@ -1801,20 +1864,16 @@ impl Workspace { ); return; } - let paths = cx.prompt_for_paths(PathPromptOptions { - files: false, - directories: true, - multiple: true, - }); + let paths = self.prompt_for_open_path( + PathPromptOptions { + files: false, + directories: true, + multiple: true, + }, + cx, + ); cx.spawn(|this, mut cx| async move { - let paths = Flatten::flatten(paths.await.map_err(|e| e.into())).map_err(|err| { - this.update(&mut cx, |this, cx| { - this.show_portal_error(err.to_string(), cx); - }) - .ok(); - err - })?; - if let Some(paths) = paths { + if let Some(paths) = paths.await.log_err().flatten() { let results = this .update(&mut cx, |this, cx| { this.open_paths(paths, OpenVisible::All, None, cx) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 235d49983b..f6a2e1e9cd 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -14,6 +14,7 @@ pub struct WorkspaceSettings { pub restore_on_startup: RestoreOnStartupBehaviour, pub drop_target_size: f32, pub when_closing_with_no_tabs: CloseWindowWhenNoItems, + pub use_system_path_prompts: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -83,6 +84,11 @@ pub struct WorkspaceSettingsContent { /// /// Default: auto ("on" on macOS, "off" otherwise) pub when_closing_with_no_tabs: Option, + /// Whether to use the system provided dialogs for Open and Save As. + /// When set to false, Zed will use the built-in keyboard-first pickers. + /// + /// Default: true + pub use_system_path_prompts: Option, } #[derive(Deserialize)]