diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 362de0fd61..ffd196bce1 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -649,7 +649,19 @@ }, { "context": "FileFinder", - "bindings": { "ctrl-shift-p": "file_finder::SelectPrev" } + "bindings": { + "ctrl-shift-p": "file_finder::SelectPrev", + "ctrl-k": "file_finder::OpenMenu" + } + }, + { + "context": "FileFinder && menu_open", + "bindings": { + "u": "pane::SplitUp", + "d": "pane::SplitDown", + "l": "pane::SplitLeft", + "r": "pane::SplitRight" + } }, { "context": "TabSwitcher", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a4b49428c8..2dd537d5c3 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -649,7 +649,19 @@ }, { "context": "FileFinder", - "bindings": { "cmd-shift-p": "file_finder::SelectPrev" } + "bindings": { + "cmd-shift-p": "file_finder::SelectPrev", + "cmd-k": "file_finder::OpenMenu" + } + }, + { + "context": "FileFinder && menu_open", + "bindings": { + "u": "pane::SplitUp", + "d": "pane::SplitDown", + "l": "pane::SplitLeft", + "r": "pane::SplitRight" + } }, { "context": "TabSwitcher", diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ce0e385057..b25a436937 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -15,8 +15,8 @@ use file_icons::FileIcons; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, - FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, - View, ViewContext, VisualContext, WeakView, + FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, + Styled, Task, View, ViewContext, VisualContext, WeakView, }; use new_path_prompt::NewPathPrompt; use open_path_prompt::OpenPathPrompt; @@ -32,16 +32,30 @@ use std::{ }, }; use text::Point; -use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; +use ui::{ + prelude::*, ButtonLike, ContextMenu, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, + PopoverMenu, PopoverMenuHandle, TintColor, +}; use util::{paths::PathWithPosition, post_inc, ResultExt}; -use workspace::{item::PreviewTabsSettings, notifications::NotifyResultExt, ModalView, Workspace}; +use workspace::{ + item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection, + Workspace, +}; -actions!(file_finder, [SelectPrev]); +actions!(file_finder, [SelectPrev, OpenMenu]); -impl ModalView for FileFinder {} +impl ModalView for FileFinder { + fn on_before_dismiss(&mut self, cx: &mut ViewContext) -> workspace::DismissDecision { + let submenu_focused = self.picker.update(cx, |picker, cx| { + picker.delegate.popover_menu_handle.is_focused(cx) + }); + workspace::DismissDecision::Dismiss(!submenu_focused) + } +} pub struct FileFinder { picker: View>, + picker_focus_handle: FocusHandle, init_modifiers: Option, } @@ -142,8 +156,14 @@ impl FileFinder { } fn new(delegate: FileFinderDelegate, cx: &mut ViewContext) -> Self { + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + let picker_focus_handle = picker.focus_handle(cx); + picker.update(cx, |picker, _| { + picker.delegate.focus_handle = picker_focus_handle.clone(); + }); Self { - picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)), + picker, + picker_focus_handle, init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()), } } @@ -168,23 +188,85 @@ impl FileFinder { self.init_modifiers = Some(cx.modifiers()); cx.dispatch_action(Box::new(menu::SelectPrev)); } + + fn handle_open_menu(&mut self, _: &OpenMenu, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + let menu_handle = &picker.delegate.popover_menu_handle; + if !menu_handle.is_deployed() { + menu_handle.show(cx); + } + }); + } + + fn go_to_file_split_left(&mut self, _: &pane::SplitLeft, cx: &mut ViewContext) { + self.go_to_file_split_inner(SplitDirection::Left, cx) + } + + fn go_to_file_split_right(&mut self, _: &pane::SplitRight, cx: &mut ViewContext) { + self.go_to_file_split_inner(SplitDirection::Right, cx) + } + + fn go_to_file_split_up(&mut self, _: &pane::SplitUp, cx: &mut ViewContext) { + self.go_to_file_split_inner(SplitDirection::Up, cx) + } + + fn go_to_file_split_down(&mut self, _: &pane::SplitDown, cx: &mut ViewContext) { + self.go_to_file_split_inner(SplitDirection::Down, cx) + } + + fn go_to_file_split_inner( + &mut self, + split_direction: SplitDirection, + cx: &mut ViewContext, + ) { + self.picker.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + if let Some(workspace) = delegate.workspace.upgrade() { + if let Some(m) = delegate.matches.get(delegate.selected_index()) { + let path = match &m { + Match::History { path, .. } => { + let worktree_id = path.project.worktree_id; + ProjectPath { + worktree_id, + path: Arc::clone(&path.project.path), + } + } + Match::Search(m) => ProjectPath { + worktree_id: WorktreeId::from_usize(m.0.worktree_id), + path: m.0.path.clone(), + }, + }; + let open_task = workspace.update(cx, move |workspace, cx| { + workspace.split_path_preview(path, false, Some(split_direction), cx) + }); + open_task.detach_and_log_err(cx); + } + } + }) + } } impl EventEmitter for FileFinder {} impl FocusableView for FileFinder { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.picker_focus_handle.clone() } } impl Render for FileFinder { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let key_context = self.picker.read(cx).delegate.key_context(cx); v_flex() - .key_context("FileFinder") + .key_context(key_context) .w(rems(34.)) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) + .on_action(cx.listener(Self::handle_open_menu)) + .on_action(cx.listener(Self::go_to_file_split_left)) + .on_action(cx.listener(Self::go_to_file_split_right)) + .on_action(cx.listener(Self::go_to_file_split_up)) + .on_action(cx.listener(Self::go_to_file_split_down)) .child(self.picker.clone()) } } @@ -205,6 +287,8 @@ pub struct FileFinderDelegate { history_items: Vec, separate_history: bool, first_update: bool, + popover_menu_handle: PopoverMenuHandle, + focus_handle: FocusHandle, } /// Use a custom ordering for file finder: the regular one @@ -533,6 +617,8 @@ impl FileFinderDelegate { history_items, separate_history, first_update: true, + popover_menu_handle: PopoverMenuHandle::default(), + focus_handle: cx.focus_handle(), } } @@ -845,6 +931,15 @@ impl FileFinderDelegate { 0 } + + fn key_context(&self, cx: &WindowContext) -> KeyContext { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("FileFinder"); + if self.popover_menu_handle.is_focused(cx) { + key_context.add("menu_open"); + } + key_context + } } impl PickerDelegate for FileFinderDelegate { @@ -958,7 +1053,7 @@ impl PickerDelegate for FileFinderDelegate { let allow_preview = PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; if secondary { - workspace.split_path_preview(project_path, allow_preview, cx) + workspace.split_path_preview(project_path, allow_preview, None, cx) } else { workspace.open_path_preview( project_path, @@ -1125,6 +1220,60 @@ impl PickerDelegate for FileFinderDelegate { ), ) } + + fn render_footer(&self, cx: &mut ViewContext>) -> Option { + let menu_open = self.popover_menu_handle.is_focused(cx); + Some( + h_flex() + .w_full() + .border_t_1() + .py_2() + .pr_2() + .border_color(cx.theme().colors().border) + .justify_end() + .child( + ButtonLike::new("open-selection") + .when_some(KeyBinding::for_action(&menu::Confirm, cx), |button, key| { + button.child(key) + }) + .child(Label::new("Open")) + .on_click(|_, cx| cx.dispatch_action(menu::Confirm.boxed_clone())), + ) + .child( + div().pl_2().child( + PopoverMenu::new("menu-popover") + .with_handle(self.popover_menu_handle.clone()) + .attach(gpui::AnchorCorner::TopRight) + .anchor(gpui::AnchorCorner::BottomRight) + .trigger( + ButtonLike::new("menu-trigger") + .selected(menu_open) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .when_some( + KeyBinding::for_action_in( + &OpenMenu, + &self.focus_handle, + cx, + ), + |button, key| button.child(key), + ) + .child(Label::new("More actions")), + ) + .menu({ + move |cx| { + Some(ContextMenu::build(cx, move |menu, _| { + menu.action("Split left", pane::SplitLeft.boxed_clone()) + .action("Split right", pane::SplitRight.boxed_clone()) + .action("Split up", pane::SplitUp.boxed_clone()) + .action("Split down", pane::SplitDown.boxed_clone()) + })) + } + }), + ), + ) + .into_any(), + ) + } } #[cfg(test)] diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index e05888bdf6..547b8f5a83 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -66,7 +66,7 @@ impl PopoverMenuHandle { .map_or(false, |state| state.menu.borrow().as_ref().is_some()) } - pub fn is_focused(&self, cx: &mut WindowContext) -> bool { + pub fn is_focused(&self, cx: &WindowContext) -> bool { self.0.borrow().as_ref().map_or(false, |state| { state .menu diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 02926e7572..17d1caf414 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2631,13 +2631,14 @@ impl Workspace { path: impl Into, cx: &mut ViewContext, ) -> Task, anyhow::Error>> { - self.split_path_preview(path, false, cx) + self.split_path_preview(path, false, None, cx) } pub fn split_path_preview( &mut self, path: impl Into, allow_preview: bool, + split_direction: Option, cx: &mut ViewContext, ) -> Task, anyhow::Error>> { let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { @@ -2658,7 +2659,8 @@ impl Workspace { let (project_entry_id, build_item) = task.await?; this.update(&mut cx, move |this, cx| -> Option<_> { let pane = pane.upgrade()?; - let new_pane = this.split_pane(pane, SplitDirection::Right, cx); + let new_pane = + this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx); new_pane.update(cx, |new_pane, cx| { Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item)) })