diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bff66cb..0f4b576 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3519,22 +3519,7 @@ impl Editor { self.selection_mark_mode = false; self.selection_drag_state = SelectionDragState::None; - if self.dismiss_menus_and_popups(true, window, cx) { - cx.notify(); - return; - } - if self.clear_expanded_diff_hunks(cx) { - cx.notify(); - return; - } - if self.show_git_blame_gutter - && !self - .blame - .as_ref() - .is_some_and(|blame| blame.read(cx).is_static()) - { - self.show_git_blame_gutter = false; - cx.notify(); + if self.dismiss_overlays(window, cx) { return; } @@ -3553,6 +3538,28 @@ impl Editor { cx.propagate(); } + /// Closes what [`Cancel`] closes before it gets to the selections: menus and popovers, + /// expanded diff hunks and the git blame gutter. Returns whether anything was closed. + pub fn dismiss_overlays(&mut self, window: &mut Window, cx: &mut Context) -> bool { + let mut dismissed = + self.dismiss_menus_and_popups(true, window, cx) || self.clear_expanded_diff_hunks(cx); + if !dismissed + && self.show_git_blame_gutter + && !self + .blame + .as_ref() + .is_some_and(|blame| blame.read(cx).is_static()) + { + self.show_git_blame_gutter = false; + dismissed = true; + } + + if dismissed { + cx.notify(); + } + dismissed + } + pub fn dismiss_menus_and_popups( &mut self, is_user_requested: bool, diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 9b2ecf8..67d4b7e 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1879,7 +1879,7 @@ mod test { use futures::StreamExt; use std::{fmt::Write, time::Duration}; - use editor::{HighlightKey, MultiBufferOffset}; + use editor::{Anchor, HighlightKey, MultiBufferOffset}; use gpui::{ Bounds, DispatchEventResult, ElementInputHandler, KeyBinding, KeyDownEvent, Keystroke, PlatformInput, PlatformInputHandler, UpdateGlobal, VisualTestContext, @@ -4687,6 +4687,45 @@ mod test { assert_eq!(cx.active_operator(), None); } + #[gpui::test] + async fn test_helix_escape_collapses_expanded_hunk(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + let selected = indoc! {" + one + «twoˇ» + three"}; + cx.set_state(selected, Mode::HelixNormal); + cx.set_head_text(indoc! {" + one + CHANGED + three"}); + cx.run_until_parked(); + + let has_expanded_hunks = |cx: &mut VimTestContext| { + cx.update_editor(|editor, _, cx| { + editor + .buffer() + .read(cx) + .has_expanded_diff_hunks_in_ranges(&[Anchor::Min..Anchor::Max], cx) + }) + }; + + cx.simulate_keystrokes("g o"); + assert!(has_expanded_hunks(&mut cx)); + + cx.simulate_keystrokes("escape"); + assert!( + !has_expanded_hunks(&mut cx), + "escape should collapse the expanded hunk" + ); + cx.assert_state(selected, Mode::HelixNormal); + + cx.simulate_keystrokes("escape"); + cx.assert_state(selected, Mode::HelixNormal); + } + #[gpui::test] async fn test_helix_rename_uses_visible_cursor_position(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_typescript(cx).await; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c016bd6..8c6c8c3 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -714,6 +714,11 @@ impl Vim { editor, cx, |vim, _: &SwitchToHelixNormalMode, window, cx| { + // Escape in helix normal mode lands here. Close popovers and overlays + // like `editor::Cancel` does in vim mode, but keep the selections. + if vim.mode == Mode::HelixNormal { + vim.update_editor(cx, |_, editor, cx| editor.dismiss_overlays(window, cx)); + } vim.switch_mode(Mode::HelixNormal, true, window, cx) }, );