build: Tear up crate graph (move terminal closer to editor) (#48602)

Piotr Osiewicz created

- **build: remove assistant_slash_commands dependency in
assistant_text_thread**
- **diagnostics: Do not depend on search**
- **Remove terminal_view's dependency on search**
- **sever breadcrumbs <-> editor dep (for the sake of terminal_view)**

Release Notes:

- N/A

Change summary

Cargo.lock                                                      |   7 
crates/assistant_slash_commands/Cargo.toml                      |   1 
crates/assistant_slash_commands/src/assistant_slash_commands.rs |   2 
crates/assistant_slash_commands/src/cargo_workspace_command.rs  | 159 ---
crates/assistant_text_thread/Cargo.toml                         |   2 
crates/assistant_text_thread/src/assistant_text_thread.rs       |   1 
crates/assistant_text_thread/src/context_server_command.rs      |   2 
crates/assistant_text_thread/src/text_thread.rs                 |   6 
crates/assistant_text_thread/src/text_thread_store.rs           |   6 
crates/breadcrumbs/Cargo.toml                                   |   4 
crates/breadcrumbs/src/breadcrumbs.rs                           |  41 
crates/diagnostics/Cargo.toml                                   |   2 
crates/diagnostics/src/toolbar_controls.rs                      |   2 
crates/editor/Cargo.toml                                        |   1 
crates/editor/src/editor.rs                                     |   1 
crates/editor/src/element.rs                                    |   2 
crates/search/Cargo.toml                                        |   1 
crates/search/src/buffer_search.rs                              |  50 
crates/search/src/buffer_search/registrar.rs                    |  55 +
crates/terminal_view/Cargo.toml                                 |   2 
crates/terminal_view/src/terminal_panel.rs                      |  32 
crates/workspace/src/toolbar.rs                                 |  14 
crates/workspace/src/workspace.rs                               |   4 
crates/zed/src/main.rs                                          |   9 
crates/zed/src/visual_test_runner.rs                            |   9 
crates/zed/src/zed.rs                                           |  10 
crates/zed/src/zed/visual_tests.rs                              |   9 
crates/zed_actions/Cargo.toml                                   |   1 
crates/zed_actions/src/lib.rs                                   |  48 
29 files changed, 220 insertions(+), 263 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -863,7 +863,6 @@ dependencies = [
  "assistant_slash_command",
  "chrono",
  "collections",
- "context_server",
  "editor",
  "feature_flags",
  "fs",
@@ -2346,7 +2345,6 @@ dependencies = [
 name = "breadcrumbs"
 version = "0.1.0"
 dependencies = [
- "editor",
  "gpui",
  "ui",
  "workspace",
@@ -4975,7 +4973,6 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rand 0.9.2",
- "search",
  "serde",
  "serde_json",
  "settings",
@@ -5506,6 +5503,7 @@ dependencies = [
  "aho-corasick",
  "anyhow",
  "assets",
+ "breadcrumbs",
  "buffer_diff",
  "client",
  "clock",
@@ -14859,7 +14857,6 @@ dependencies = [
  "menu",
  "pretty_assertions",
  "project",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -16887,7 +16884,6 @@ dependencies = [
  "rand 0.9.2",
  "regex",
  "schemars",
- "search",
  "serde",
  "serde_json",
  "settings",
@@ -21260,6 +21256,7 @@ dependencies = [
  "gpui",
  "schemars",
  "serde",
+ "util",
  "uuid",
 ]
 

crates/assistant_slash_commands/Cargo.toml 🔗

@@ -16,7 +16,6 @@ anyhow.workspace = true
 assistant_slash_command.workspace = true
 chrono.workspace = true
 collections.workspace = true
-context_server.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 fs.workspace = true

crates/assistant_slash_commands/src/assistant_slash_commands.rs 🔗

@@ -1,4 +1,3 @@
-mod context_server_command;
 mod default_command;
 mod delta_command;
 mod diagnostics_command;
@@ -11,7 +10,6 @@ mod streaming_example_command;
 mod symbols_command;
 mod tab_command;
 
-pub use crate::context_server_command::*;
 pub use crate::default_command::*;
 pub use crate::delta_command::*;
 pub use crate::diagnostics_command::*;

crates/assistant_slash_commands/src/cargo_workspace_command.rs 🔗

@@ -1,159 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
-    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
-    SlashCommandResult,
-};
-use fs::Fs;
-use gpui::{App, Entity, Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use project::{Project, ProjectPath};
-use std::{
-    fmt::Write,
-    path::Path,
-    sync::{Arc, atomic::AtomicBool},
-};
-use ui::prelude::*;
-use util::rel_path::RelPath;
-use workspace::Workspace;
-
-pub struct CargoWorkspaceSlashCommand;
-
-impl CargoWorkspaceSlashCommand {
-    async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
-        let buffer = fs.load(path_to_cargo_toml).await?;
-        let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
-
-        let mut message = String::new();
-        writeln!(message, "You are in a Rust project.")?;
-
-        if let Some(workspace) = cargo_toml.workspace {
-            writeln!(
-                message,
-                "The project is a Cargo workspace with the following members:"
-            )?;
-            for member in workspace.members {
-                writeln!(message, "- {member}")?;
-            }
-
-            if !workspace.default_members.is_empty() {
-                writeln!(message, "The default members are:")?;
-                for member in workspace.default_members {
-                    writeln!(message, "- {member}")?;
-                }
-            }
-
-            if !workspace.dependencies.is_empty() {
-                writeln!(
-                    message,
-                    "The following workspace dependencies are installed:"
-                )?;
-                for dependency in workspace.dependencies.keys() {
-                    writeln!(message, "- {dependency}")?;
-                }
-            }
-        } else if let Some(package) = cargo_toml.package {
-            writeln!(
-                message,
-                "The project name is \"{name}\".",
-                name = package.name
-            )?;
-
-            let description = package
-                .description
-                .as_ref()
-                .and_then(|description| description.get().ok().cloned());
-            if let Some(description) = description.as_ref() {
-                writeln!(message, "It describes itself as \"{description}\".")?;
-            }
-
-            if !cargo_toml.dependencies.is_empty() {
-                writeln!(message, "The following dependencies are installed:")?;
-                for dependency in cargo_toml.dependencies.keys() {
-                    writeln!(message, "- {dependency}")?;
-                }
-            }
-        }
-
-        Ok(message)
-    }
-
-    fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
-        let worktree = project.read(cx).worktrees(cx).next()?;
-        let worktree = worktree.read(cx);
-        let entry = worktree.entry_for_path(RelPath::new("Cargo.toml").unwrap())?;
-        let path = ProjectPath {
-            worktree_id: worktree.id(),
-            path: entry.path.clone(),
-        };
-        Some(Arc::from(
-            project.read(cx).absolute_path(&path, cx)?.as_path(),
-        ))
-    }
-}
-
-impl SlashCommand for CargoWorkspaceSlashCommand {
-    fn name(&self) -> String {
-        "cargo-workspace".into()
-    }
-
-    fn description(&self) -> String {
-        "insert project workspace metadata".into()
-    }
-
-    fn menu_text(&self) -> String {
-        "Insert Project Workspace Metadata".into()
-    }
-
-    fn complete_argument(
-        self: Arc<Self>,
-        _arguments: &[String],
-        _cancel: Arc<AtomicBool>,
-        _workspace: Option<WeakEntity<Workspace>>,
-        _window: &mut Window,
-        _cx: &mut App,
-    ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        Task::ready(Err(anyhow!("this command does not require argument")))
-    }
-
-    fn requires_argument(&self) -> bool {
-        false
-    }
-
-    fn run(
-        self: Arc<Self>,
-        _arguments: &[String],
-        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
-        _context_buffer: BufferSnapshot,
-        workspace: WeakEntity<Workspace>,
-        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
-        _window: &mut Window,
-        cx: &mut App,
-    ) -> Task<SlashCommandResult> {
-        let output = workspace.update(cx, |workspace, cx| {
-            let project = workspace.project().clone();
-            let fs = workspace.project().read(cx).fs().clone();
-            let path = Self::path_to_cargo_toml(project, cx);
-            let output = cx.background_spawn(async move {
-                let path = path.with_context(|| "Cargo.toml not found")?;
-                Self::build_message(fs, &path).await
-            });
-
-            cx.foreground_executor().spawn(async move {
-                let text = output.await?;
-                let range = 0..text.len();
-                Ok(SlashCommandOutput {
-                    text,
-                    sections: vec![SlashCommandOutputSection {
-                        range,
-                        icon: IconName::FileTree,
-                        label: "Project".into(),
-                        metadata: None,
-                    }],
-                    run_commands_in_text: false,
-                }
-                .into_event_stream())
-            })
-        });
-        output.unwrap_or_else(|error| Task::ready(Err(error)))
-    }
-}

crates/assistant_text_thread/Cargo.toml 🔗

@@ -18,7 +18,6 @@ test-support = []
 agent_settings.workspace = true
 anyhow.workspace = true
 assistant_slash_command.workspace = true
-assistant_slash_commands.workspace = true
 chrono.workspace = true
 client.workspace = true
 clock.workspace = true
@@ -55,6 +54,7 @@ workspace.workspace = true
 zed_env_vars.workspace = true
 
 [dev-dependencies]
+assistant_slash_commands.workspace = true
 indoc.workspace = true
 language_model = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true

crates/assistant_slash_commands/src/context_server_command.rs → crates/assistant_text_thread/src/context_server_command.rs 🔗

@@ -14,7 +14,7 @@ use text::LineEnding;
 use ui::{IconName, SharedString};
 use workspace::Workspace;
 
-use crate::create_label_for_command;
+use assistant_slash_command::create_label_for_command;
 
 pub struct ContextServerSlashCommand {
     store: Entity<ContextServerStore>,

crates/assistant_text_thread/src/text_thread.rs 🔗

@@ -4,7 +4,6 @@ use assistant_slash_command::{
     SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
     SlashCommandResult, SlashCommandWorkingSet,
 };
-use assistant_slash_commands::FileCommandMetadata;
 use client::{self, proto};
 use clock::ReplicaId;
 use cloud_llm_client::CompletionIntent;
@@ -1176,6 +1175,11 @@ impl TextThread {
     }
 
     pub fn contains_files(&self, cx: &App) -> bool {
+        // Mimics assistant_slash_commands::FileCommandMetadata.
+        #[derive(Serialize, Deserialize)]
+        pub struct FileCommandMetadata {
+            pub path: String,
+        }
         let buffer = self.buffer.read(cx);
         self.slash_command_output_sections.iter().any(|section| {
             section.is_valid(buffer)

crates/assistant_text_thread/src/text_thread_store.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     SavedTextThread, SavedTextThreadMetadata, TextThread, TextThreadEvent, TextThreadId,
-    TextThreadOperation, TextThreadVersion,
+    TextThreadOperation, TextThreadVersion, context_server_command,
 };
 use anyhow::{Context as _, Result};
 use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
@@ -938,11 +938,11 @@ impl TextThreadStore {
                 let slash_command_ids = response
                     .prompts
                     .into_iter()
-                    .filter(assistant_slash_commands::acceptable_prompt)
+                    .filter(context_server_command::acceptable_prompt)
                     .map(|prompt| {
                         log::info!("registering context server command: {:?}", prompt.name);
                         slash_command_working_set.insert(Arc::new(
-                            assistant_slash_commands::ContextServerSlashCommand::new(
+                            context_server_command::ContextServerSlashCommand::new(
                                 context_server_store.clone(),
                                 server.id(),
                                 prompt,

crates/breadcrumbs/Cargo.toml 🔗

@@ -13,12 +13,10 @@ path = "src/breadcrumbs.rs"
 doctest = false
 
 [dependencies]
-editor.workspace = true
 gpui.workspace = true
 ui.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]
-editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
-workspace = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,11 +1,25 @@
-use editor::render_breadcrumb_text;
-use gpui::{Context, EventEmitter, IntoElement, Render, Subscription, Window};
+use gpui::{
+    AnyElement, App, Context, EventEmitter, Global, IntoElement, Render, Subscription, Window,
+};
 use ui::prelude::*;
 use workspace::{
     ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
-    item::{ItemEvent, ItemHandle},
+    item::{BreadcrumbText, ItemEvent, ItemHandle},
 };
 
+type RenderBreadcrumbTextFn = fn(
+    Vec<BreadcrumbText>,
+    Option<AnyElement>,
+    &dyn ItemHandle,
+    bool,
+    &mut Window,
+    &App,
+) -> AnyElement;
+
+pub struct RenderBreadcrumbText(pub RenderBreadcrumbTextFn);
+
+impl Global for RenderBreadcrumbText {}
+
 pub struct Breadcrumbs {
     pane_focused: bool,
     active_item: Option<Box<dyn ItemHandle>>,
@@ -49,15 +63,18 @@ impl Render for Breadcrumbs {
 
         let prefix_element = active_item.breadcrumb_prefix(window, cx);
 
-        render_breadcrumb_text(
-            segments,
-            prefix_element,
-            active_item.as_ref(),
-            false,
-            window,
-            cx,
-        )
-        .into_any_element()
+        if let Some(render_fn) = cx.try_global::<RenderBreadcrumbText>() {
+            (render_fn.0)(
+                segments,
+                prefix_element,
+                active_item.as_ref(),
+                false,
+                window,
+                cx,
+            )
+        } else {
+            element.into_any_element()
+        }
     }
 }
 

crates/diagnostics/Cargo.toml 🔗

@@ -27,7 +27,7 @@ lsp.workspace = true
 markdown.workspace = true
 project.workspace = true
 rand.workspace = true
-search.workspace = true
+
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/diagnostics/src/toolbar_controls.rs 🔗

@@ -1,11 +1,11 @@
 use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
 use gpui::{Context, EventEmitter, ParentElement, Render, Window};
 use language::DiagnosticEntry;
-use search::buffer_search;
 use text::{Anchor, BufferId};
 use ui::{Tooltip, prelude::*};
 use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
 use zed_actions::assistant::InlineAssist;
+use zed_actions::buffer_search;
 
 pub struct ToolbarControls {
     editor: Option<Box<dyn DiagnosticsToolbarEditor>>,

crates/editor/Cargo.toml 🔗

@@ -33,6 +33,7 @@ test-support = [
 aho-corasick.workspace = true
 anyhow.workspace = true
 assets.workspace = true
+breadcrumbs.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true

crates/editor/src/editor.rs 🔗

@@ -328,6 +328,7 @@ pub enum HideMouseCursorOrigin {
 
 pub fn init(cx: &mut App) {
     cx.set_global(GlobalBlameRenderer(Arc::new(())));
+    cx.set_global(breadcrumbs::RenderBreadcrumbText(render_breadcrumb_text));
 
     workspace::register_project_item::<Editor>(cx);
     workspace::FollowableViewRegistry::register::<Editor>(cx);

crates/editor/src/element.rs 🔗

@@ -7776,7 +7776,7 @@ pub fn render_breadcrumb_text(
     multibuffer_header: bool,
     window: &mut Window,
     cx: &App,
-) -> impl IntoElement {
+) -> gpui::AnyElement {
     const MAX_SEGMENTS: usize = 12;
 
     let element = h_flex().flex_grow().text_ui(cx);

crates/search/Cargo.toml 🔗

@@ -32,7 +32,6 @@ gpui.workspace = true
 language.workspace = true
 menu.workspace = true
 project.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -22,15 +22,14 @@ use futures::channel::oneshot;
 use gpui::{
     Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
     IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
-    WeakEntity, Window, actions, div,
+    WeakEntity, Window, div,
 };
 use language::{Language, LanguageRegistry};
 use project::{
     search::SearchQuery,
     search_history::{SearchHistory, SearchHistoryCursor},
 };
-use schemars::JsonSchema;
-use serde::Deserialize;
+
 use settings::Settings;
 use std::{any::TypeId, sync::Arc};
 use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
@@ -46,53 +45,12 @@ use workspace::{
     },
 };
 
-pub use registrar::DivRegistrar;
+pub use registrar::{DivRegistrar, register_pane_search_actions};
 use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};
 
 const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
 
-/// Opens the buffer search interface with the specified configuration.
-#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
-#[action(namespace = buffer_search)]
-#[serde(deny_unknown_fields)]
-pub struct Deploy {
-    #[serde(default = "util::serde::default_true")]
-    pub focus: bool,
-    #[serde(default)]
-    pub replace_enabled: bool,
-    #[serde(default)]
-    pub selection_search_enabled: bool,
-}
-
-actions!(
-    buffer_search,
-    [
-        /// Deploys the search and replace interface.
-        DeployReplace,
-        /// Dismisses the search bar.
-        Dismiss,
-        /// Focuses back on the editor.
-        FocusEditor
-    ]
-);
-
-impl Deploy {
-    pub fn find() -> Self {
-        Self {
-            focus: true,
-            replace_enabled: false,
-            selection_search_enabled: false,
-        }
-    }
-
-    pub fn replace() -> Self {
-        Self {
-            focus: true,
-            replace_enabled: true,
-            selection_search_enabled: false,
-        }
-    }
-}
+pub use zed_actions::buffer_search::{Deploy, DeployReplace, Dismiss, FocusEditor};
 
 pub enum Event {
     UpdateLocation,

crates/search/src/buffer_search/registrar.rs 🔗

@@ -1,5 +1,5 @@
-use gpui::{Action, Context, Div, Entity, InteractiveElement, Window, div};
-use workspace::Workspace;
+use gpui::{Action, App, Context, Div, Entity, InteractiveElement, Window, div};
+use workspace::{Pane, Workspace};
 
 use crate::BufferSearchBar;
 
@@ -58,6 +58,57 @@ impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
     }
 }
 
+pub struct PaneDivRegistrar {
+    div: Option<Div>,
+    pane: Entity<Pane>,
+}
+
+impl PaneDivRegistrar {
+    pub fn new(div: Div, pane: Entity<Pane>) -> Self {
+        Self {
+            div: Some(div),
+            pane,
+        }
+    }
+
+    pub fn into_div(self) -> Div {
+        self.div.unwrap()
+    }
+}
+
+impl SearchActionsRegistrar for PaneDivRegistrar {
+    fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
+        let pane = self.pane.clone();
+        self.div = self.div.take().map(|div| {
+            div.on_action(move |action: &A, window: &mut Window, cx: &mut App| {
+                let search_bar = pane
+                    .read(cx)
+                    .toolbar()
+                    .read(cx)
+                    .item_of_type::<BufferSearchBar>();
+                let should_notify = search_bar
+                    .map(|search_bar| {
+                        search_bar.update(cx, |search_bar, cx| {
+                            callback.execute(search_bar, action, window, cx)
+                        })
+                    })
+                    .unwrap_or(false);
+                if should_notify {
+                    pane.update(cx, |_, cx| cx.notify());
+                } else {
+                    cx.propagate();
+                }
+            })
+        });
+    }
+}
+
+pub fn register_pane_search_actions(div: Div, pane: Entity<Pane>) -> Div {
+    let mut registrar = PaneDivRegistrar::new(div, pane);
+    BufferSearchBar::register(&mut registrar);
+    registrar.into_div()
+}
+
 /// Register actions for an active pane.
 impl SearchActionsRegistrar for Workspace {
     fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {

crates/terminal_view/Cargo.toml 🔗

@@ -35,7 +35,7 @@ project.workspace = true
 regex.workspace = true
 task.workspace = true
 schemars.workspace = true
-search.workspace = true
+
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use project::{Fs, Project, ProjectEntryId};
-use search::{BufferSearchBar, buffer_search::DivRegistrar};
+
 use settings::{Settings, TerminalDockPosition};
 use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
 use terminal::{Terminal, terminal_settings::TerminalSettings};
@@ -1238,12 +1238,13 @@ pub fn new_terminal_pane(
             false
         })));
 
-        let buffer_search_bar = cx.new(|cx| {
-            search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
-        });
+        let toolbar = pane.toolbar().clone();
+        if let Some(callbacks) = cx.try_global::<workspace::PaneSearchBarCallbacks>() {
+            let languages = Some(project.read(cx).languages().clone());
+            (callbacks.setup_search_bar)(languages, &toolbar, window, cx);
+        }
         let breadcrumbs = cx.new(|_| Breadcrumbs::new());
-        pane.toolbar().update(cx, |toolbar, cx| {
-            toolbar.add_item(buffer_search_bar, window, cx);
+        toolbar.update(cx, |toolbar, cx| {
             toolbar.add_item(breadcrumbs, window, cx);
         });
 
@@ -1483,19 +1484,12 @@ impl EventEmitter<PanelEvent> for TerminalPanel {}
 
 impl Render for TerminalPanel {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let mut registrar = DivRegistrar::new(
-            |panel, _, cx| {
-                panel
-                    .active_pane
-                    .read(cx)
-                    .toolbar()
-                    .read(cx)
-                    .item_of_type::<BufferSearchBar>()
-            },
-            cx,
-        );
-        BufferSearchBar::register(&mut registrar);
-        let registrar = registrar.into_div();
+        let registrar = cx
+            .try_global::<workspace::PaneSearchBarCallbacks>()
+            .map(|callbacks| {
+                (callbacks.wrap_div_with_search_actions)(div(), self.active_pane.clone())
+            })
+            .unwrap_or_else(div);
         self.workspace
             .update(cx, |workspace, cx| {
                 registrar.size_full().child(self.center.render(

crates/workspace/src/toolbar.rs 🔗

@@ -1,11 +1,21 @@
 use crate::ItemHandle;
 use gpui::{
-    AnyView, App, Context, Entity, EntityId, EventEmitter, KeyContext, ParentElement as _, Render,
-    Styled, Window,
+    AnyView, App, Context, Div, Entity, EntityId, EventEmitter, Global, KeyContext,
+    ParentElement as _, Render, Styled, Window,
 };
+use language::LanguageRegistry;
+use std::sync::Arc;
 use ui::prelude::*;
 use ui::{h_flex, v_flex};
 
+pub struct PaneSearchBarCallbacks {
+    pub setup_search_bar:
+        fn(Option<Arc<LanguageRegistry>>, &Entity<Toolbar>, &mut Window, &mut App),
+    pub wrap_div_with_search_actions: fn(Div, Entity<crate::Pane>) -> Div,
+}
+
+impl Global for PaneSearchBarCallbacks {}
+
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),

crates/workspace/src/workspace.rs 🔗

@@ -118,7 +118,9 @@ use std::{
 };
 use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
 use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings};
-pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
+pub use toolbar::{
+    PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
+};
 pub use ui;
 use ui::{Window, prelude::*};
 use util::{

crates/zed/src/main.rs 🔗

@@ -659,6 +659,15 @@ fn main() {
         snippets_ui::init(cx);
         channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
         search::init(cx);
+        cx.set_global(workspace::PaneSearchBarCallbacks {
+            setup_search_bar: |languages, toolbar, window, cx| {
+                let search_bar = cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
+                toolbar.update(cx, |toolbar, cx| {
+                    toolbar.add_item(search_bar, window, cx);
+                });
+            },
+            wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
+        });
         vim::init(cx);
         terminal_view::init(cx);
         journal::init(app_state.clone(), cx);

crates/zed/src/visual_test_runner.rs 🔗

@@ -187,6 +187,15 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
         terminal_view::init(cx);
         image_viewer::init(cx);
         search::init(cx);
+        cx.set_global(workspace::PaneSearchBarCallbacks {
+            setup_search_bar: |languages, toolbar, window, cx| {
+                let search_bar = cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
+                toolbar.update(cx, |toolbar, cx| {
+                    toolbar.add_item(search_bar, window, cx);
+                });
+            },
+            wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
+        });
         prompt_store::init(cx);
         language_model::init(app_state.client.clone(), cx);
         language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);

crates/zed/src/zed.rs 🔗

@@ -5067,6 +5067,16 @@ mod tests {
             debugger_ui::init(cx);
             initialize_workspace(app_state.clone(), prompt_builder, cx);
             search::init(cx);
+            cx.set_global(workspace::PaneSearchBarCallbacks {
+                setup_search_bar: |languages, toolbar, window, cx| {
+                    let search_bar =
+                        cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
+                    toolbar.update(cx, |toolbar, cx| {
+                        toolbar.add_item(search_bar, window, cx);
+                    });
+                },
+                wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
+            });
             app_state
         })
     }

crates/zed/src/zed/visual_tests.rs 🔗

@@ -62,6 +62,15 @@ pub fn init_visual_test(cx: &mut VisualTestAppContext) -> Arc<AppState> {
         terminal_view::init(cx);
         image_viewer::init(cx);
         search::init(cx);
+        cx.set_global(workspace::PaneSearchBarCallbacks {
+            setup_search_bar: |languages, toolbar, window, cx| {
+                let search_bar = cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
+                toolbar.update(cx, |toolbar, cx| {
+                    toolbar.add_item(search_bar, window, cx);
+                });
+            },
+            wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
+        });
 
         app_state
     })

crates/zed_actions/Cargo.toml 🔗

@@ -12,4 +12,5 @@ workspace = true
 gpui.workspace = true
 schemars.workspace = true
 serde.workspace = true
+util.workspace = true
 uuid.workspace = true

crates/zed_actions/src/lib.rs 🔗

@@ -363,6 +363,54 @@ pub mod search {
         ]
     );
 }
+pub mod buffer_search {
+    use gpui::{Action, actions};
+    use schemars::JsonSchema;
+    use serde::Deserialize;
+
+    /// Opens the buffer search interface with the specified configuration.
+    #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
+    #[action(namespace = buffer_search)]
+    #[serde(deny_unknown_fields)]
+    pub struct Deploy {
+        #[serde(default = "util::serde::default_true")]
+        pub focus: bool,
+        #[serde(default)]
+        pub replace_enabled: bool,
+        #[serde(default)]
+        pub selection_search_enabled: bool,
+    }
+
+    impl Deploy {
+        pub fn find() -> Self {
+            Self {
+                focus: true,
+                replace_enabled: false,
+                selection_search_enabled: false,
+            }
+        }
+
+        pub fn replace() -> Self {
+            Self {
+                focus: true,
+                replace_enabled: true,
+                selection_search_enabled: false,
+            }
+        }
+    }
+
+    actions!(
+        buffer_search,
+        [
+            /// Deploys the search and replace interface.
+            DeployReplace,
+            /// Dismisses the search bar.
+            Dismiss,
+            /// Focuses back on the editor.
+            FocusEditor
+        ]
+    );
+}
 pub mod settings_profile_selector {
     use gpui::Action;
     use schemars::JsonSchema;