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
This commit is contained in:
Cole Miller 2025-01-08 09:29:15 -05:00 committed by GitHub
parent 36301442dd
commit bbb473b8df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 95 additions and 38 deletions

View file

@ -1418,6 +1418,11 @@ impl AppContext {
pub fn get_name(&self) -> &'static str { pub fn get_name(&self) -> &'static str {
self.name.as_ref().unwrap() 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 { impl Context for AppContext {

View file

@ -175,6 +175,7 @@ pub(crate) trait Platform: 'static {
options: PathPromptOptions, options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>; ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>; fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
fn can_select_mixed_files_and_dirs(&self) -> bool;
fn reveal_path(&self, path: &Path); fn reveal_path(&self, path: &Path);
fn open_with_system(&self, path: &Path); fn open_with_system(&self, path: &Path);

View file

@ -372,6 +372,11 @@ impl<P: LinuxClient + 'static> Platform for P {
done_rx 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) { fn reveal_path(&self, path: &Path) {
self.reveal_path(path.to_owned()); self.reveal_path(path.to_owned());
} }

View file

@ -759,6 +759,10 @@ impl Platform for MacPlatform {
done_rx done_rx
} }
fn can_select_mixed_files_and_dirs(&self) -> bool {
true
}
fn reveal_path(&self, path: &Path) { fn reveal_path(&self, path: &Path) {
unsafe { unsafe {
let path = path.to_path_buf(); let path = path.to_path_buf();

View file

@ -299,6 +299,10 @@ impl Platform for TestPlatform {
rx rx
} }
fn can_select_mixed_files_and_dirs(&self) -> bool {
true
}
fn reveal_path(&self, _path: &std::path::Path) { fn reveal_path(&self, _path: &std::path::Path) {
unimplemented!() unimplemented!()
} }

View file

@ -407,6 +407,11 @@ impl Platform for WindowsPlatform {
rx 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) { fn reveal_path(&self, path: &Path) {
let Ok(file_full_path) = path.canonicalize() else { let Ok(file_full_path) = path.canonicalize() else {
log::error!("unable to parse file path"); log::error!("unable to parse file path");

View file

@ -449,6 +449,10 @@ where
); );
} }
pub fn log_err<E: std::fmt::Debug>(error: &E) {
log_error_with_caller(*Location::caller(), error, log::Level::Warn);
}
pub trait TryFutureExt { pub trait TryFutureExt {
fn log_err(self) -> LogErrorFuture<Self> fn log_err(self) -> LogErrorFuture<Self>
where where

View file

@ -34,10 +34,10 @@ use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext, transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId, AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
WindowHandle, WindowId, WindowOptions, WindowId, WindowOptions,
}; };
pub use item::{ pub use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@ -145,6 +145,7 @@ actions!(
NewTerminal, NewTerminal,
NewWindow, NewWindow,
Open, Open,
OpenFiles,
OpenInTerminal, OpenInTerminal,
ReloadActiveItem, ReloadActiveItem,
SaveAs, SaveAs,
@ -332,6 +333,42 @@ pub fn init_settings(cx: &mut AppContext) {
TabBarSettings::register(cx); TabBarSettings::register(cx);
} }
fn prompt_and_open_paths(
app_state: Arc<AppState>,
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>())
{
workspace_window
.update(cx, |workspace, cx| {
workspace.show_portal_error(err.to_string(), cx);
})
.ok();
}
})
.ok();
}
}
})
.detach();
}
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx); init_settings(cx);
notifications::init(cx); notifications::init(cx);
@ -343,41 +380,33 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.on_action({ cx.on_action({
let app_state = Arc::downgrade(&app_state); let app_state = Arc::downgrade(&app_state);
move |_: &Open, cx: &mut AppContext| { 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() { if let Some(app_state) = app_state.upgrade() {
cx.spawn(move |cx| async move { prompt_and_open_paths(
match Flatten::flatten(paths.await.map_err(|e| e.into())) { app_state,
Ok(Some(paths)) => { PathPromptOptions {
cx.update(|cx| { files: true,
open_paths(&paths, app_state, OpenOptions::default(), cx) directories: true,
.detach_and_log_err(cx) multiple: true,
}) },
.ok(); cx,
} );
Ok(None) => {} }
Err(err) => { }
cx.update(|cx| { });
if let Some(workspace_window) = cx cx.on_action({
.active_window() let app_state = Arc::downgrade(&app_state);
.and_then(|window| window.downcast::<Workspace>()) move |_: &OpenFiles, cx: &mut AppContext| {
{ let directories = cx.can_select_mixed_files_and_dirs();
workspace_window if let Some(app_state) = app_state.upgrade() {
.update(cx, |workspace, cx| { prompt_and_open_paths(
workspace.show_portal_error(err.to_string(), cx); app_state,
}) PathPromptOptions {
.ok(); files: true,
} directories,
}) multiple: true,
.ok(); },
} cx,
}; );
})
.detach();
} }
} }
}); });