diff --git a/Cargo.lock b/Cargo.lock index 14528bfd52b03de5bbf4b0acb19aca525019b800..69269aaf82cea606270192885a9fca0df3b0747d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,6 +391,7 @@ dependencies = [ "rope", "rules_library", "schemars", + "search", "semver", "serde", "serde_json", @@ -415,6 +416,7 @@ dependencies = [ "url", "util", "uuid", + "vim", "watch", "workspace", "zed_actions", diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index add2415b4cde0d8cf623e91bff8f2dd5463527ea..64114c4f2a4b10b0566250ba772b6b3188b14123 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -135,8 +135,10 @@ remote = { workspace = true, features = ["test-support"] } remote_connection = { workspace = true, features = ["test-support"] } remote_server = { workspace = true, features = ["test-support"] } +search = { workspace = true, features = ["test-support"] } semver.workspace = true reqwest_client.workspace = true tempfile.workspace = true +vim.workspace = true tree-sitter-md.workspace = true unindent.workspace = true diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d04c5567a410ffb1d724f3fb3823e0c89ddddabf..380a1dd4ef8a46c763c5ff80fff758ad0b976afb 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -8052,6 +8052,103 @@ mod tests { ); }); } + #[gpui::test] + async fn test_vim_search_does_not_steal_focus_from_agent_panel(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + vim::init(cx); + search::init(cx); + + // Enable vim mode + settings::SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |s| s.vim_mode = Some(true)); + }); + + // Load vim keybindings + let mut vim_key_bindings = + settings::KeymapFile::load_asset_allow_partial_failure("keymaps/vim.json", cx) + .unwrap(); + for key_binding in &mut vim_key_bindings { + key_binding.set_meta(settings::KeybindSource::Vim.meta()); + } + cx.bind_keys(vim_key_bindings); + }); + + // Create a project with a file so we have a buffer in the center pane. + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({ "file.txt": "hello world" })) + .await; + let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace + .read_with(cx, |mw, _cx| mw.workspace().clone()) + .unwrap(); + let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx); + + // Open a file in the center pane. + workspace + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_paths( + vec![PathBuf::from("/project/file.txt")], + workspace::OpenOptions::default(), + None, + window, + cx, + ) + }) + .await; + cx.run_until_parked(); + + // Add a BufferSearchBar to the center pane's toolbar, as a real + // workspace would have. + workspace.update_in(&mut cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let search_bar = cx.new(|cx| search::BufferSearchBar::new(None, window, cx)); + toolbar.add_item(search_bar, window, cx); + }); + }); + }); + + // Create the agent panel and add it to the workspace. + let panel = workspace.update_in(&mut cx, |workspace, window, cx| { + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + + // Open a thread so the panel has an active editor. + open_thread_with_connection(&panel, StubAgentConnection::new(), &mut cx); + + // Focus the agent panel. + workspace.update_in(&mut cx, |workspace, window, cx| { + workspace.focus_panel::(window, cx); + }); + cx.run_until_parked(); + + // Verify the agent panel has focus. + workspace.update_in(&mut cx, |_, window, cx| { + assert!( + panel.read(cx).focus_handle(cx).contains_focused(window, cx), + "Agent panel should be focused before pressing '/'" + ); + }); + + // Press '/' — the vim search keybinding. + cx.simulate_keystrokes("/"); + + // Focus should remain on the agent panel. + workspace.update_in(&mut cx, |_, window, cx| { + assert!( + panel.read(cx).focus_handle(cx).contains_focused(window, cx), + "Focus should remain on the agent panel after pressing '/'" + ); + }); + } /// Connection that tracks closed sessions and detects prompts against /// sessions that no longer exist, used to reproduce session disassociation. diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 917641423f89c6da82576ae512cd01bea82ce4ab..fea9735f27b36bcc0b56e7ba8155e9f51a767813 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -29,8 +29,8 @@ use editor::{ movement::{self, FindRange}, }; use gpui::{ - Action, App, AppContext, Axis, Context, Entity, EventEmitter, KeyContext, KeystrokeEvent, - Render, Subscription, Task, WeakEntity, Window, actions, + Action, App, AppContext, Axis, Context, Entity, EventEmitter, Focusable, KeyContext, + KeystrokeEvent, Render, Subscription, Task, WeakEntity, Window, actions, }; use insert::{NormalBefore, TemporaryNormal}; use language::{ @@ -1043,8 +1043,17 @@ impl Vim { } pub fn pane(&self, window: &Window, cx: &Context) -> Option> { - self.workspace(window, cx) - .map(|workspace| workspace.read(cx).focused_pane(window, cx)) + let pane = self + .workspace(window, cx) + .map(|workspace| workspace.read(cx).focused_pane(window, cx))?; + // `focused_pane` falls back to the center pane when a dock panel + // without its own pane (e.g. the Agent panel) has focus. Guard + // against that so vim search/match commands don't steal focus. + if pane.read(cx).focus_handle(cx).contains_focused(window, cx) { + Some(pane) + } else { + None + } } pub fn enabled(cx: &mut App) -> bool {