From bbb473b8dffaeac64945264207f62e71a8275c28 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 8 Jan 2025 09:29:15 -0500 Subject: [PATCH] Add a dedicated action to open files (#22625) Closes #22531 Closes #22250 Closes #15679 Release Notes: - Add `workspace::OpenFiles` action to enable opening individual files on Linux and Windows --- crates/gpui/src/app.rs | 5 + crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/linux/platform.rs | 5 + crates/gpui/src/platform/mac/platform.rs | 4 + crates/gpui/src/platform/test/platform.rs | 4 + crates/gpui/src/platform/windows/platform.rs | 5 + crates/util/src/util.rs | 4 + crates/workspace/src/workspace.rs | 105 ++++++++++++------- 8 files changed, 95 insertions(+), 38 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 070ff29c21555e0cb91807c1e5081b2f6eff5819..a0ec1f9933f112d6db90daab6a011f995101fe6f 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 e01d3e61f02617575e7548859b10c893badd71d2..5a4f9b93a0906d936ea9a6b76630da856a648e21 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 e897b5eb48e3f591fe46a583cefd9d3b4cfa6984..d5823e091ad7c1d8f46c337816a12980481ae14f 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 096bf860a6921a72cd0f3b6a387bc9022c5c35d9..da645750a5c1efe86bb9ee897cd389d058a1ac6a 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 67227b60fec0477be9fa7930c625193b3f730b08..50ad24a520e52ed56940c9b8afdfedcc23fe4715 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 d29261076807181a23fae4b09a4368c926317543..b01851ab86a8a1de18593c6bed0029c39256850c 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 fe5e77c0aa873999aa23abc7e9ded86212134a53..6c6196756cfd005dc04bf41e745aaefdb0a7ad33 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 e9676dd4a07e88318148160974ca368b38526143..25fa708c7be4891a527e6685cff00683d427c3d1 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, + ); } } });