Detailed changes
@@ -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",
@@ -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
@@ -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::<AgentPanel>(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.
@@ -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<Self>) -> Option<Entity<Pane>> {
- 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 {