diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 070ff29c21..a0ec1f9933 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1418,6 +1418,11 @@ impl AppContext { pub fn get_name(&self) -> &'static str { self.name.as_ref().unwrap() } + + /// Returns `true` if the platform file picker supports selecting a mix of files and directories. + pub fn can_select_mixed_files_and_dirs(&self) -> bool { + self.platform.can_select_mixed_files_and_dirs() + } } impl Context for AppContext { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e01d3e61f0..5a4f9b93a0 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -175,6 +175,7 @@ pub(crate) trait Platform: 'static { options: PathPromptOptions, ) -> oneshot::Receiver>>>; fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>>; + fn can_select_mixed_files_and_dirs(&self) -> bool; fn reveal_path(&self, path: &Path); fn open_with_system(&self, path: &Path); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index e897b5eb48..d5823e091a 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -372,6 +372,11 @@ impl Platform for P { done_rx } + fn can_select_mixed_files_and_dirs(&self) -> bool { + // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories". + false + } + fn reveal_path(&self, path: &Path) { self.reveal_path(path.to_owned()); } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 096bf860a6..da645750a5 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -759,6 +759,10 @@ impl Platform for MacPlatform { done_rx } + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + fn reveal_path(&self, path: &Path) { unsafe { let path = path.to_path_buf(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 67227b60fe..50ad24a520 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -299,6 +299,10 @@ impl Platform for TestPlatform { rx } + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + fn reveal_path(&self, _path: &std::path::Path) { unimplemented!() } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index d292610768..b01851ab86 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -407,6 +407,11 @@ impl Platform for WindowsPlatform { rx } + fn can_select_mixed_files_and_dirs(&self) -> bool { + // The FOS_PICKFOLDERS flag toggles between "only files" and "only folders". + false + } + fn reveal_path(&self, path: &Path) { let Ok(file_full_path) = path.canonicalize() else { log::error!("unable to parse file path"); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index fe5e77c0aa..6c6196756c 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -449,6 +449,10 @@ where ); } +pub fn log_err(error: &E) { + log_error_with_caller(*Location::caller(), error, log::Level::Warn); +} + pub trait TryFutureExt { fn log_err(self) -> LogErrorFuture where diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e9676dd4a0..25fa708c7b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -34,10 +34,10 @@ use gpui::{ action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId, - EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, - ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, - ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, - WindowHandle, WindowId, WindowOptions, + EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView, + Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, + Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle, + WindowId, WindowOptions, }; pub use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -145,6 +145,7 @@ actions!( NewTerminal, NewWindow, Open, + OpenFiles, OpenInTerminal, ReloadActiveItem, SaveAs, @@ -332,6 +333,42 @@ pub fn init_settings(cx: &mut AppContext) { TabBarSettings::register(cx); } +fn prompt_and_open_paths( + app_state: Arc, + options: PathPromptOptions, + cx: &mut AppContext, +) { + let paths = cx.prompt_for_paths(options); + cx.spawn(|cx| async move { + match paths.await.anyhow().and_then(|res| res) { + Ok(Some(paths)) => { + cx.update(|cx| { + open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx) + }) + .ok(); + } + Ok(None) => {} + Err(err) => { + util::log_err(&err); + cx.update(|cx| { + if let Some(workspace_window) = cx + .active_window() + .and_then(|window| window.downcast::()) + { + workspace_window + .update(cx, |workspace, cx| { + workspace.show_portal_error(err.to_string(), cx); + }) + .ok(); + } + }) + .ok(); + } + } + }) + .detach(); +} + pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); @@ -343,41 +380,33 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.on_action({ let app_state = Arc::downgrade(&app_state); move |_: &Open, cx: &mut AppContext| { - let paths = cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: true, - multiple: true, - }); - if let Some(app_state) = app_state.upgrade() { - cx.spawn(move |cx| async move { - match Flatten::flatten(paths.await.map_err(|e| e.into())) { - Ok(Some(paths)) => { - cx.update(|cx| { - open_paths(&paths, app_state, OpenOptions::default(), cx) - .detach_and_log_err(cx) - }) - .ok(); - } - Ok(None) => {} - Err(err) => { - cx.update(|cx| { - if let Some(workspace_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - workspace_window - .update(cx, |workspace, cx| { - workspace.show_portal_error(err.to_string(), cx); - }) - .ok(); - } - }) - .ok(); - } - }; - }) - .detach(); + prompt_and_open_paths( + app_state, + PathPromptOptions { + files: true, + directories: true, + multiple: true, + }, + cx, + ); + } + } + }); + cx.on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &OpenFiles, cx: &mut AppContext| { + let directories = cx.can_select_mixed_files_and_dirs(); + if let Some(app_state) = app_state.upgrade() { + prompt_and_open_paths( + app_state, + PathPromptOptions { + files: true, + directories, + multiple: true, + }, + cx, + ); } } });