Fix conflicts

R Aadarsh created

Change summary

Cargo.lock                                         |   68 
crates/agent/Cargo.toml                            |    1 
crates/agent/src/tools/edit_file_tool.rs           |    4 
crates/agent2/Cargo.toml                           |  104 
crates/agent_ui/src/acp/message_editor.rs          |   93 
crates/assistant_tools/Cargo.toml                  |   94 
crates/assistant_tools/src/edit_file_tool.rs       | 2440 ----------------
crates/edit_prediction_context/src/syntax_index.rs |    2 
crates/editor/src/editor_settings.rs               |   12 
crates/encodings_ui/Cargo.toml                     |    1 
crates/encodings_ui/src/lib.rs                     |    4 
crates/git_ui/src/file_diff_view.rs                |    1 
crates/language/Cargo.toml                         |    1 
crates/project/src/buffer_store.rs                 |   11 
crates/project/src/project.rs                      |   11 
crates/project/src/project_search.rs               |  754 ++++
crates/project/src/project_tests.rs                |   29 
crates/remote_server/src/headless_project.rs       |   11 
crates/remote_server/src/remote_editing_tests.rs   |    2 
crates/worktree/src/worktree.rs                    |    6 
crates/worktree/src/worktree_tests.rs              |   99 
crates/zed/Cargo.toml                              |    1 
crates/zed/src/zed/open_listener.rs                |    3 
crates/zeta/src/zeta.rs                            |    2 
24 files changed, 878 insertions(+), 2,876 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5514,6 +5514,70 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
 
+[[package]]
+name = "encoding"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
+dependencies = [
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
+]
+
+[[package]]
+name = "encoding-index-japanese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-korean"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-simpchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-singlebyte"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-tradchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding_index_tests"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.35"
@@ -5548,7 +5612,6 @@ dependencies = [
  "ui",
  "util",
  "workspace",
- "workspace-hack",
  "zed_actions",
 ]
 
@@ -6429,6 +6492,7 @@ dependencies = [
  "async-trait",
  "cocoa 0.26.0",
  "collections",
+ "encoding",
  "encodings",
  "fsevent",
  "futures 0.3.31",
@@ -8806,6 +8870,7 @@ dependencies = [
  "ctor",
  "diffy",
  "ec4rs",
+ "encoding",
  "encodings",
  "fs",
  "futures 0.3.31",
@@ -20806,6 +20871,7 @@ dependencies = [
  "async-lock 2.8.0",
  "clock",
  "collections",
+ "encoding",
  "encodings",
  "fs",
  "futures 0.3.31",

crates/agent/Cargo.toml 🔗

@@ -32,6 +32,7 @@ collections.workspace = true
 context_server.workspace = true
 db.workspace = true
 derive_more.workspace = true
+encodings.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true

crates/agent/src/tools/edit_file_tool.rs 🔗

@@ -563,7 +563,7 @@ mod tests {
     use super::*;
     use crate::{ContextServerRegistry, Templates};
     use client::TelemetrySettings;
-    use encodings::{Encoding, UTF_8};
+    use encodings::Encoding;
     use fs::Fs;
     use gpui::{TestAppContext, UpdateGlobal};
     use language_model::fake_provider::FakeLanguageModel;
@@ -745,7 +745,7 @@ mod tests {
             path!("/root/src/main.rs").as_ref(),
             &Rope::from_str_small("initial content"),
             language::LineEnding::Unix,
-            Encoding::new(UTF_8),
+            Encoding::default(),
         )
         .await
         .unwrap();

crates/agent2/Cargo.toml 🔗

@@ -1,104 +0,0 @@
-[package]
-name = "agent2"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lib]
-path = "src/agent2.rs"
-
-[features]
-test-support = ["db/test-support"]
-e2e = []
-
-[lints]
-workspace = true
-
-[dependencies]
-acp_thread.workspace = true
-action_log.workspace = true
-agent.workspace = true
-agent-client-protocol.workspace = true
-agent_servers.workspace = true
-agent_settings.workspace = true
-anyhow.workspace = true
-assistant_context.workspace = true
-assistant_tool.workspace = true
-assistant_tools.workspace = true
-chrono.workspace = true
-client.workspace = true
-cloud_llm_client.workspace = true
-collections.workspace = true
-context_server.workspace = true
-db.workspace = true
-encodings.workspace = true
-fs.workspace = true
-futures.workspace = true
-git.workspace = true
-gpui.workspace = true
-handlebars = { workspace = true, features = ["rust-embed"] }
-html_to_markdown.workspace = true
-http_client.workspace = true
-indoc.workspace = true
-itertools.workspace = true
-language.workspace = true
-language_model.workspace = true
-language_models.workspace = true
-log.workspace = true
-open.workspace = true
-parking_lot.workspace = true
-paths.workspace = true
-project.workspace = true
-prompt_store.workspace = true
-rust-embed.workspace = true
-schemars.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-smol.workspace = true
-sqlez.workspace = true
-task.workspace = true
-telemetry.workspace = true
-terminal.workspace = true
-thiserror.workspace = true
-text.workspace = true
-ui.workspace = true
-util.workspace = true
-uuid.workspace = true
-watch.workspace = true
-web_search.workspace = true
-workspace-hack.workspace = true
-zed_env_vars.workspace = true
-zstd.workspace = true
-
-
-[dev-dependencies]
-agent = { workspace = true, "features" = ["test-support"] }
-agent_servers = { workspace = true, "features" = ["test-support"] }
-assistant_context = { workspace = true, "features" = ["test-support"] }
-ctor.workspace = true
-client = { workspace = true, "features" = ["test-support"] }
-clock = { workspace = true, "features" = ["test-support"] }
-context_server = { workspace = true, "features" = ["test-support"] }
-db = { workspace = true, "features" = ["test-support"] }
-editor = { workspace = true, "features" = ["test-support"] }
-env_logger.workspace = true
-fs = { workspace = true, "features" = ["test-support"] }
-git = { workspace = true, "features" = ["test-support"] }
-gpui = { workspace = true, "features" = ["test-support"] }
-gpui_tokio.workspace = true
-language = { workspace = true, "features" = ["test-support"] }
-language_model = { workspace = true, "features" = ["test-support"] }
-lsp = { workspace = true, "features" = ["test-support"] }
-pretty_assertions.workspace = true
-project = { workspace = true, "features" = ["test-support"] }
-reqwest_client.workspace = true
-settings = { workspace = true, "features" = ["test-support"] }
-tempfile.workspace = true
-terminal = { workspace = true, "features" = ["test-support"] }
-theme = { workspace = true, "features" = ["test-support"] }
-tree-sitter-rust.workspace = true
-unindent = { workspace = true }
-worktree = { workspace = true, "features" = ["test-support"] }
-zlog.workspace = true

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -463,97 +463,6 @@ impl MessageEditor {
         })
     }
 
-    fn confirm_mention_for_directory(
-        &mut self,
-        abs_path: PathBuf,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
-            let mut files = Vec::new();
-
-            for entry in worktree.child_entries(path) {
-                if entry.is_dir() {
-                    files.extend(collect_files_in_path(worktree, &entry.path));
-                } else if entry.is_file() {
-                    files.push((entry.path.clone(), worktree.full_path(&entry.path)));
-                }
-            }
-
-            files
-        }
-
-        let Some(project_path) = self
-            .project
-            .read(cx)
-            .project_path_for_absolute_path(&abs_path, cx)
-        else {
-            return Task::ready(Err(anyhow!("project path not found")));
-        };
-        let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
-            return Task::ready(Err(anyhow!("project entry not found")));
-        };
-        let directory_path = entry.path.clone();
-        let worktree_id = project_path.worktree_id;
-        let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
-            return Task::ready(Err(anyhow!("worktree not found")));
-        };
-        let project = self.project.clone();
-        cx.spawn(async move |_, cx| {
-            let file_paths = worktree.read_with(cx, |worktree, _cx| {
-                collect_files_in_path(worktree, &directory_path)
-            })?;
-            let descendants_future = cx.update(|cx| {
-                join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
-                    let rel_path = worktree_path
-                        .strip_prefix(&directory_path)
-                        .log_err()
-                        .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
-
-                    let open_task = project.update(cx, |project, cx| {
-                        project.buffer_store().update(cx, |buffer_store, cx| {
-                            let project_path = ProjectPath {
-                                worktree_id,
-                                path: worktree_path,
-                            };
-                            buffer_store.open_buffer(project_path, None, false, true, cx)
-                        })
-                    });
-
-                    cx.spawn(async move |cx| {
-                        let buffer = open_task.await.log_err()?;
-                        let buffer_content = outline::get_buffer_content_or_outline(
-                            buffer.clone(),
-                            Some(&full_path),
-                            &cx,
-                        )
-                        .await
-                        .ok()?;
-
-                        Some((rel_path, full_path, buffer_content.text, buffer))
-                    })
-                }))
-            })?;
-
-            let contents = cx
-                .background_spawn(async move {
-                    let (contents, tracked_buffers) = descendants_future
-                        .await
-                        .into_iter()
-                        .flatten()
-                        .map(|(rel_path, full_path, rope, buffer)| {
-                            ((rel_path, full_path, rope), buffer)
-                        })
-                        .unzip();
-                    Mention::Text {
-                        content: render_directory_contents(contents),
-                        tracked_buffers,
-                    }
-                })
-                .await;
-            anyhow::Ok(contents)
-        })
-    }
-
     fn confirm_mention_for_fetch(
         &mut self,
         url: url::Url,
@@ -1339,7 +1248,7 @@ fn full_mention_for_directory(
                             worktree_id,
                             path: worktree_path,
                         };
-                        buffer_store.open_buffer(project_path, cx)
+                        buffer_store.open_buffer(project_path, None, false, true, cx)
                     })
                 });
 

crates/assistant_tools/Cargo.toml 🔗

@@ -1,94 +0,0 @@
-[package]
-name = "assistant_tools"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_tools.rs"
-
-[features]
-eval = []
-
-[dependencies]
-action_log.workspace = true
-agent_settings.workspace = true
-anyhow.workspace = true
-assistant_tool.workspace = true
-buffer_diff.workspace = true
-chrono.workspace = true
-client.workspace = true
-cloud_llm_client.workspace = true
-collections.workspace = true
-component.workspace = true
-derive_more.workspace = true
-diffy = "0.4.2"
-editor.workspace = true
-encodings.workspace = true
-feature_flags.workspace = true
-futures.workspace = true
-gpui.workspace = true
-handlebars = { workspace = true, features = ["rust-embed"] }
-html_to_markdown.workspace = true
-http_client.workspace = true
-indoc.workspace = true
-itertools.workspace = true
-language.workspace = true
-language_model.workspace = true
-log.workspace = true
-lsp.workspace = true
-markdown.workspace = true
-open.workspace = true
-paths.workspace = true
-portable-pty.workspace = true
-project.workspace = true
-prompt_store.workspace = true
-regex.workspace = true
-rust-embed.workspace = true
-schemars.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-smallvec.workspace = true
-streaming_diff.workspace = true
-strsim.workspace = true
-task.workspace = true
-terminal.workspace = true
-terminal_view.workspace = true
-theme.workspace = true
-ui.workspace = true
-util.workspace = true
-watch.workspace = true
-web_search.workspace = true
-which.workspace = true
-workspace-hack.workspace = true
-workspace.workspace = true
-
-[dev-dependencies]
-lsp = { workspace = true, features = ["test-support"] }
-client = { workspace = true, features = ["test-support"] }
-clock = { workspace = true, features = ["test-support"] }
-collections = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-gpui_tokio.workspace = true
-fs = { workspace = true, features = ["test-support"] }
-language = { workspace = true, features = ["test-support"] }
-language_model = { workspace = true, features = ["test-support"] }
-language_models.workspace = true
-project = { workspace = true, features = ["test-support"] }
-rand.workspace = true
-pretty_assertions.workspace = true
-reqwest_client.workspace = true
-settings = { workspace = true, features = ["test-support"] }
-smol.workspace = true
-task = { workspace = true, features = ["test-support"]}
-tempfile.workspace = true
-theme.workspace = true
-tree-sitter-rust.workspace = true
-workspace = { workspace = true, features = ["test-support"] }
-unindent.workspace = true
-zlog.workspace = true

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -1,2440 +0,0 @@
-use crate::{
-    Templates,
-    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
-    schema::json_schema_for,
-    ui::{COLLAPSED_LINES, ToolOutputPreview},
-};
-use action_log::ActionLog;
-use agent_settings;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{
-    AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
-};
-use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{
-    Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
-};
-use futures::StreamExt;
-use gpui::{
-    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
-    TextStyleRefinement, WeakEntity, pulsating_between, px,
-};
-use indoc::formatdoc;
-use language::{
-    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
-    TextBuffer,
-    language_settings::{self, FormatOnSave, SoftWrap},
-};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use paths;
-use project::{
-    Project, ProjectPath,
-    lsp_store::{FormatTrigger, LspFormatTarget},
-};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use std::{
-    cmp::Reverse,
-    collections::HashSet,
-    ops::Range,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
-use theme::ThemeSettings;
-use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
-use util::ResultExt;
-use workspace::Workspace;
-
-pub struct EditFileTool;
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct EditFileToolInput {
-    /// A one-line, user-friendly markdown description of the edit. This will be
-    /// shown in the UI and also passed to another model to perform the edit.
-    ///
-    /// Be terse, but also descriptive in what you want to achieve with this
-    /// edit. Avoid generic instructions.
-    ///
-    /// NEVER mention the file path in this description.
-    ///
-    /// <example>Fix API endpoint URLs</example>
-    /// <example>Update copyright year in `page_footer`</example>
-    ///
-    /// Make sure to include this field before all the others in the input object
-    /// so that we can display it immediately.
-    pub display_description: String,
-
-    /// The full path of the file to create or modify in the project.
-    ///
-    /// WARNING: When specifying which file path need changing, you MUST
-    /// start each path with one of the project's root directories.
-    ///
-    /// The following examples assume we have two root directories in the project:
-    /// - /a/b/backend
-    /// - /c/d/frontend
-    ///
-    /// <example>
-    /// `backend/src/main.rs`
-    ///
-    /// Notice how the file path starts with `backend`. Without that, the path
-    /// would be ambiguous and the call would fail!
-    /// </example>
-    ///
-    /// <example>
-    /// `frontend/db.js`
-    /// </example>
-    pub path: PathBuf,
-
-    /// The mode of operation on the file. Possible values:
-    /// - 'edit': Make granular edits to an existing file.
-    /// - 'create': Create a new file if it doesn't exist.
-    /// - 'overwrite': Replace the entire contents of an existing file.
-    ///
-    /// When a file already exists or you just created it, prefer editing
-    /// it as opposed to recreating it from scratch.
-    pub mode: EditFileMode,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum EditFileMode {
-    Edit,
-    Create,
-    Overwrite,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct EditFileToolOutput {
-    pub original_path: PathBuf,
-    pub new_text: String,
-    pub old_text: Arc<String>,
-    pub raw_output: Option<EditAgentOutput>,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-struct PartialInput {
-    #[serde(default)]
-    path: String,
-    #[serde(default)]
-    display_description: String,
-}
-
-const DEFAULT_UI_TEXT: &str = "Editing file";
-
-impl Tool for EditFileTool {
-    fn name(&self) -> String {
-        "edit_file".into()
-    }
-
-    fn needs_confirmation(
-        &self,
-        input: &serde_json::Value,
-        project: &Entity<Project>,
-        cx: &App,
-    ) -> bool {
-        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
-            return false;
-        }
-
-        let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
-            // If it's not valid JSON, it's going to error and confirming won't do anything.
-            return false;
-        };
-
-        // If any path component matches the local settings folder, then this could affect
-        // the editor in ways beyond the project source, so prompt.
-        let local_settings_folder = paths::local_settings_folder_relative_path();
-        let path = Path::new(&input.path);
-        if path
-            .components()
-            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
-        {
-            return true;
-        }
-
-        // It's also possible that the global config dir is configured to be inside the project,
-        // so check for that edge case too.
-        if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
-            && canonical_path.starts_with(paths::config_dir())
-        {
-            return true;
-        }
-
-        // Check if path is inside the global config directory
-        // First check if it's already inside project - if not, try to canonicalize
-        let project_path = project.read(cx).find_project_path(&input.path, cx);
-
-        // If the path is inside the project, and it's not one of the above edge cases,
-        // then no confirmation is necessary. Otherwise, confirmation is necessary.
-        project_path.is_none()
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn description(&self) -> String {
-        include_str!("edit_file_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolPencil
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<EditFileToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<EditFileToolInput>(input.clone()) {
-            Ok(input) => {
-                let path = Path::new(&input.path);
-                let mut description = input.display_description.clone();
-
-                // Add context about why confirmation may be needed
-                let local_settings_folder = paths::local_settings_folder_relative_path();
-                if path
-                    .components()
-                    .any(|c| c.as_os_str() == local_settings_folder.as_os_str())
-                {
-                    description.push_str(" (local settings)");
-                } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
-                    && canonical_path.starts_with(paths::config_dir())
-                {
-                    description.push_str(" (global settings)");
-                }
-
-                description
-            }
-            Err(_) => "Editing file".to_string(),
-        }
-    }
-
-    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
-        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
-            let description = input.display_description.trim();
-            if !description.is_empty() {
-                return description.to_string();
-            }
-
-            let path = input.path.trim();
-            if !path.is_empty() {
-                return path.to_string();
-            }
-        }
-
-        DEFAULT_UI_TEXT.to_string()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        model: Arc<dyn LanguageModel>,
-        window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<EditFileToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let project_path = match resolve_path(&input, project.clone(), cx) {
-            Ok(path) => path,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let card = window.and_then(|window| {
-            window
-                .update(cx, |_, window, cx| {
-                    cx.new(|cx| {
-                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
-                    })
-                })
-                .ok()
-        });
-
-        let card_clone = card.clone();
-        let action_log_clone = action_log.clone();
-        let task = cx.spawn(async move |cx: &mut AsyncApp| {
-            let edit_format = EditFormat::from_model(model.clone())?;
-            let edit_agent = EditAgent::new(
-                model,
-                project.clone(),
-                action_log_clone,
-                Templates::new(),
-                edit_format,
-            );
-
-            let buffer = project
-                .update(cx, |project, cx| {
-                    project.open_buffer(project_path.clone(), cx)
-                })?
-                .await?;
-
-            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
-            let old_text = cx
-                .background_spawn({
-                    let old_snapshot = old_snapshot.clone();
-                    async move { Arc::new(old_snapshot.text()) }
-                })
-                .await;
-
-            if let Some(card) = card_clone.as_ref() {
-                card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;
-            }
-
-            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
-                edit_agent.edit(
-                    buffer.clone(),
-                    input.display_description.clone(),
-                    &request,
-                    cx,
-                )
-            } else {
-                edit_agent.overwrite(
-                    buffer.clone(),
-                    input.display_description.clone(),
-                    &request,
-                    cx,
-                )
-            };
-
-            let mut hallucinated_old_text = false;
-            let mut ambiguous_ranges = Vec::new();
-            while let Some(event) = events.next().await {
-                match event {
-                    EditAgentOutputEvent::Edited { .. } => {
-                        if let Some(card) = card_clone.as_ref() {
-                            card.update(cx, |card, cx| card.update_diff(cx))?;
-                        }
-                    }
-                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
-                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
-                    EditAgentOutputEvent::ResolvingEditRange(range) => {
-                        if let Some(card) = card_clone.as_ref() {
-                            card.update(cx, |card, cx| card.reveal_range(range, cx))?;
-                        }
-                    }
-                }
-            }
-            let agent_output = output.await?;
-
-            // If format_on_save is enabled, format the buffer
-            let format_on_save_enabled = buffer
-                .read_with(cx, |buffer, cx| {
-                    let settings = language_settings::language_settings(
-                        buffer.language().map(|l| l.name()),
-                        buffer.file(),
-                        cx,
-                    );
-                    !matches!(settings.format_on_save, FormatOnSave::Off)
-                })
-                .unwrap_or(false);
-
-            if format_on_save_enabled {
-                action_log.update(cx, |log, cx| {
-                    log.buffer_edited(buffer.clone(), cx);
-                })?;
-                let format_task = project.update(cx, |project, cx| {
-                    project.format(
-                        HashSet::from_iter([buffer.clone()]),
-                        LspFormatTarget::Buffers,
-                        false, // Don't push to history since the tool did it.
-                        FormatTrigger::Save,
-                        cx,
-                    )
-                })?;
-                format_task.await.log_err();
-            }
-
-            project
-                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
-                .await?;
-
-            // Notify the action log that we've edited the buffer (*after* formatting has completed).
-            action_log.update(cx, |log, cx| {
-                log.buffer_edited(buffer.clone(), cx);
-            })?;
-
-            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
-            let (new_text, diff) = cx
-                .background_spawn({
-                    let new_snapshot = new_snapshot.clone();
-                    let old_text = old_text.clone();
-                    async move {
-                        let new_text = new_snapshot.text();
-                        let diff = language::unified_diff(&old_text, &new_text);
-
-                        (new_text, diff)
-                    }
-                })
-                .await;
-
-            let output = EditFileToolOutput {
-                original_path: project_path.path.to_path_buf(),
-                new_text,
-                old_text,
-                raw_output: Some(agent_output),
-            };
-
-            if let Some(card) = card_clone {
-                card.update(cx, |card, cx| {
-                    card.update_diff(cx);
-                    card.finalize(cx)
-                })
-                .log_err();
-            }
-
-            let input_path = input.path.display();
-            if diff.is_empty() {
-                anyhow::ensure!(
-                    !hallucinated_old_text,
-                    formatdoc! {"
-                        Some edits were produced but none of them could be applied.
-                        Read the relevant sections of {input_path} again so that
-                        I can perform the requested edits.
-                    "}
-                );
-                anyhow::ensure!(
-                    ambiguous_ranges.is_empty(),
-                    {
-                        let line_numbers = ambiguous_ranges
-                            .iter()
-                            .map(|range| range.start.to_string())
-                            .collect::<Vec<_>>()
-                            .join(", ");
-                        formatdoc! {"
-                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
-                            relevant sections of {input_path} again and extend <old_text> so
-                            that I can perform the requested edits.
-                        "}
-                    }
-                );
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text("No edits were made.".into()),
-                    output: serde_json::to_value(output).ok(),
-                })
-            } else {
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text(format!(
-                        "Edited {}:\n\n```diff\n{}\n```",
-                        input_path, diff
-                    )),
-                    output: serde_json::to_value(output).ok(),
-                })
-            }
-        });
-
-        ToolResult {
-            output: task,
-            card: card.map(AnyToolCard::from),
-        }
-    }
-
-    fn deserialize_card(
-        self: Arc<Self>,
-        output: serde_json::Value,
-        project: Entity<Project>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Option<AnyToolCard> {
-        let output = match serde_json::from_value::<EditFileToolOutput>(output) {
-            Ok(output) => output,
-            Err(_) => return None,
-        };
-
-        let card = cx.new(|cx| {
-            EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)
-        });
-
-        cx.spawn({
-            let path: Arc<Path> = output.original_path.into();
-            let language_registry = project.read(cx).languages().clone();
-            let card = card.clone();
-            async move |cx| {
-                let buffer =
-                    build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;
-                let buffer_diff =
-                    build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)
-                        .await?;
-                card.update(cx, |card, cx| {
-                    card.multibuffer.update(cx, |multibuffer, cx| {
-                        let snapshot = buffer.read(cx).snapshot();
-                        let diff = buffer_diff.read(cx);
-                        let diff_hunk_ranges = diff
-                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
-                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
-                            .collect::<Vec<_>>();
-
-                        multibuffer.set_excerpts_for_path(
-                            PathKey::for_buffer(&buffer, cx),
-                            buffer,
-                            diff_hunk_ranges,
-                            multibuffer_context_lines(cx),
-                            cx,
-                        );
-                        multibuffer.add_diff(buffer_diff, cx);
-                        let end = multibuffer.len(cx);
-                        card.total_lines =
-                            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);
-                    });
-
-                    cx.notify();
-                })?;
-                anyhow::Ok(())
-            }
-        })
-        .detach_and_log_err(cx);
-
-        Some(card.into())
-    }
-}
-
-/// Validate that the file path is valid, meaning:
-///
-/// - For `edit` and `overwrite`, the path must point to an existing file.
-/// - For `create`, the file must not already exist, but it's parent dir must exist.
-fn resolve_path(
-    input: &EditFileToolInput,
-    project: Entity<Project>,
-    cx: &mut App,
-) -> Result<ProjectPath> {
-    let project = project.read(cx);
-
-    match input.mode {
-        EditFileMode::Edit | EditFileMode::Overwrite => {
-            let path = project
-                .find_project_path(&input.path, cx)
-                .context("Can't edit file: path not found")?;
-
-            let entry = project
-                .entry_for_path(&path, cx)
-                .context("Can't edit file: path not found")?;
-
-            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
-            Ok(path)
-        }
-
-        EditFileMode::Create => {
-            if let Some(path) = project.find_project_path(&input.path, cx) {
-                anyhow::ensure!(
-                    project.entry_for_path(&path, cx).is_none(),
-                    "Can't create file: file already exists"
-                );
-            }
-
-            let parent_path = input
-                .path
-                .parent()
-                .context("Can't create file: incorrect path")?;
-
-            let parent_project_path = project.find_project_path(&parent_path, cx);
-
-            let parent_entry = parent_project_path
-                .as_ref()
-                .and_then(|path| project.entry_for_path(path, cx))
-                .context("Can't create file: parent directory doesn't exist")?;
-
-            anyhow::ensure!(
-                parent_entry.is_dir(),
-                "Can't create file: parent is not a directory"
-            );
-
-            let file_name = input
-                .path
-                .file_name()
-                .context("Can't create file: invalid filename")?;
-
-            let new_file_path = parent_project_path.map(|parent| ProjectPath {
-                path: Arc::from(parent.path.join(file_name)),
-                ..parent
-            });
-
-            new_file_path.context("Can't create file")
-        }
-    }
-}
-
-pub struct EditFileToolCard {
-    path: PathBuf,
-    editor: Entity<Editor>,
-    multibuffer: Entity<MultiBuffer>,
-    project: Entity<Project>,
-    buffer: Option<Entity<Buffer>>,
-    base_text: Option<Arc<String>>,
-    buffer_diff: Option<Entity<BufferDiff>>,
-    revealed_ranges: Vec<Range<Anchor>>,
-    diff_task: Option<Task<Result<()>>>,
-    preview_expanded: bool,
-    error_expanded: Option<Entity<Markdown>>,
-    full_height_expanded: bool,
-    total_lines: Option<u32>,
-}
-
-impl EditFileToolCard {
-    pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
-        let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
-        let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
-
-        let editor = cx.new(|cx| {
-            let mut editor = Editor::new(
-                EditorMode::Full {
-                    scale_ui_elements_with_buffer_font_size: false,
-                    show_active_line_background: false,
-                    sized_by_content: true,
-                },
-                multibuffer.clone(),
-                Some(project.clone()),
-                window,
-                cx,
-            );
-            editor.set_show_gutter(false, cx);
-            editor.disable_inline_diagnostics();
-            editor.disable_expand_excerpt_buttons(cx);
-            // Keep horizontal scrollbar so user can scroll horizontally if needed
-            editor.set_show_vertical_scrollbar(false, cx);
-            editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
-            editor.set_soft_wrap_mode(SoftWrap::None, cx);
-            editor.scroll_manager.set_forbid_vertical_scroll(true);
-            editor.set_show_indent_guides(false, cx);
-            editor.set_read_only(true);
-            editor.set_show_breakpoints(false, cx);
-            editor.set_show_code_actions(false, cx);
-            editor.set_show_git_diff_gutter(false, cx);
-            editor.set_expand_all_diff_hunks(cx);
-            editor
-        });
-        Self {
-            path,
-            project,
-            editor,
-            multibuffer,
-            buffer: None,
-            base_text: None,
-            buffer_diff: None,
-            revealed_ranges: Vec::new(),
-            diff_task: None,
-            preview_expanded: true,
-            error_expanded: None,
-            full_height_expanded: expand_edit_card,
-            total_lines: None,
-        }
-    }
-
-    pub fn initialize(&mut self, buffer: Entity<Buffer>, cx: &mut App) {
-        let buffer_snapshot = buffer.read(cx).snapshot();
-        let base_text = buffer_snapshot.text();
-        let language_registry = buffer.read(cx).language_registry();
-        let text_snapshot = buffer.read(cx).text_snapshot();
-
-        // Create a buffer diff with the current text as the base
-        let buffer_diff = cx.new(|cx| {
-            let mut diff = BufferDiff::new(&text_snapshot, cx);
-            let _ = diff.set_base_text(
-                buffer_snapshot.clone(),
-                language_registry,
-                text_snapshot,
-                cx,
-            );
-            diff
-        });
-
-        self.buffer = Some(buffer);
-        self.base_text = Some(base_text.into());
-        self.buffer_diff = Some(buffer_diff.clone());
-
-        // Add the diff to the multibuffer
-        self.multibuffer
-            .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx));
-    }
-
-    pub fn is_loading(&self) -> bool {
-        self.total_lines.is_none()
-    }
-
-    pub fn update_diff(&mut self, cx: &mut Context<Self>) {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return;
-        };
-        let Some(buffer_diff) = self.buffer_diff.as_ref() else {
-            return;
-        };
-
-        let buffer = buffer.clone();
-        let buffer_diff = buffer_diff.clone();
-        let base_text = self.base_text.clone();
-        self.diff_task = Some(cx.spawn(async move |this, cx| {
-            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
-            let diff_snapshot = BufferDiff::update_diff(
-                buffer_diff.clone(),
-                text_snapshot.clone(),
-                base_text,
-                false,
-                false,
-                None,
-                None,
-                cx,
-            )
-            .await?;
-            buffer_diff.update(cx, |diff, cx| {
-                diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
-            })?;
-            this.update(cx, |this, cx| this.update_visible_ranges(cx))
-        }));
-    }
-
-    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
-        self.revealed_ranges.push(range);
-        self.update_visible_ranges(cx);
-    }
-
-    fn update_visible_ranges(&mut self, cx: &mut Context<Self>) {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return;
-        };
-
-        let ranges = self.excerpt_ranges(cx);
-        self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.set_excerpts_for_path(
-                PathKey::for_buffer(buffer, cx),
-                buffer.clone(),
-                ranges,
-                multibuffer_context_lines(cx),
-                cx,
-            );
-            let end = multibuffer.len(cx);
-            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
-        });
-        cx.notify();
-    }
-
-    fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return Vec::new();
-        };
-        let Some(diff) = self.buffer_diff.as_ref() else {
-            return Vec::new();
-        };
-
-        let buffer = buffer.read(cx);
-        let diff = diff.read(cx);
-        let mut ranges = diff
-            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
-            .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
-            .collect::<Vec<_>>();
-        ranges.extend(
-            self.revealed_ranges
-                .iter()
-                .map(|range| range.to_point(buffer)),
-        );
-        ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
-
-        // Merge adjacent ranges
-        let mut ranges = ranges.into_iter().peekable();
-        let mut merged_ranges = Vec::new();
-        while let Some(mut range) = ranges.next() {
-            while let Some(next_range) = ranges.peek() {
-                if range.end >= next_range.start {
-                    range.end = range.end.max(next_range.end);
-                    ranges.next();
-                } else {
-                    break;
-                }
-            }
-
-            merged_ranges.push(range);
-        }
-        merged_ranges
-    }
-
-    pub fn finalize(&mut self, cx: &mut Context<Self>) -> Result<()> {
-        let ranges = self.excerpt_ranges(cx);
-        let buffer = self.buffer.take().context("card was already finalized")?;
-        let base_text = self
-            .base_text
-            .take()
-            .context("card was already finalized")?;
-        let language_registry = self.project.read(cx).languages().clone();
-
-        // Replace the buffer in the multibuffer with the snapshot
-        let buffer = cx.new(|cx| {
-            let language = buffer.read(cx).language().cloned();
-            let buffer = TextBuffer::new_normalized(
-                0,
-                cx.entity_id().as_non_zero_u64().into(),
-                buffer.read(cx).line_ending(),
-                buffer.read(cx).as_rope().clone(),
-            );
-            let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
-            buffer.set_language(language, cx);
-            buffer
-        });
-
-        let buffer_diff = cx.spawn({
-            let buffer = buffer.clone();
-            async move |_this, cx| {
-                build_buffer_diff(base_text, &buffer, &language_registry, cx).await
-            }
-        });
-
-        cx.spawn(async move |this, cx| {
-            let buffer_diff = buffer_diff.await?;
-            this.update(cx, |this, cx| {
-                this.multibuffer.update(cx, |multibuffer, cx| {
-                    let path_key = PathKey::for_buffer(&buffer, cx);
-                    multibuffer.clear(cx);
-                    multibuffer.set_excerpts_for_path(
-                        path_key,
-                        buffer,
-                        ranges,
-                        multibuffer_context_lines(cx),
-                        cx,
-                    );
-                    multibuffer.add_diff(buffer_diff.clone(), cx);
-                });
-
-                cx.notify();
-            })
-        })
-        .detach_and_log_err(cx);
-        Ok(())
-    }
-}
-
-impl ToolCard for EditFileToolCard {
-    fn render(
-        &mut self,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let error_message = match status {
-            ToolUseStatus::Error(err) => Some(err),
-            _ => None,
-        };
-
-        let running_or_pending = match status {
-            ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
-            _ => None,
-        };
-
-        let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
-
-        let path_label_button = h_flex()
-            .id(("edit-tool-path-label-button", self.editor.entity_id()))
-            .w_full()
-            .max_w_full()
-            .px_1()
-            .gap_0p5()
-            .cursor_pointer()
-            .rounded_sm()
-            .opacity(0.8)
-            .hover(|label| {
-                label
-                    .opacity(1.)
-                    .bg(cx.theme().colors().element_hover.opacity(0.5))
-            })
-            .tooltip(Tooltip::text("Jump to File"))
-            .child(
-                h_flex()
-                    .child(
-                        Icon::new(IconName::ToolPencil)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(
-                        div()
-                            .text_size(rems(0.8125))
-                            .child(self.path.display().to_string())
-                            .ml_1p5()
-                            .mr_0p5(),
-                    )
-                    .child(
-                        Icon::new(IconName::ArrowUpRight)
-                            .size(IconSize::Small)
-                            .color(Color::Ignored),
-                    ),
-            )
-            .on_click({
-                let path = self.path.clone();
-                move |_, window, cx| {
-                    workspace
-                        .update(cx, {
-                            |workspace, cx| {
-                                let Some(project_path) =
-                                    workspace.project().read(cx).find_project_path(&path, cx)
-                                else {
-                                    return;
-                                };
-                                let open_task =
-                                    workspace.open_path(project_path, None, true, window, cx);
-                                window
-                                    .spawn(cx, async move |cx| {
-                                        let item = open_task.await?;
-                                        if let Some(active_editor) = item.downcast::<Editor>() {
-                                            active_editor
-                                                .update_in(cx, |editor, window, cx| {
-                                                    let snapshot =
-                                                        editor.buffer().read(cx).snapshot(cx);
-                                                    let first_hunk = editor
-                                                        .diff_hunks_in_ranges(
-                                                            &[editor::Anchor::min()
-                                                                ..editor::Anchor::max()],
-                                                            &snapshot,
-                                                        )
-                                                        .next();
-                                                    if let Some(first_hunk) = first_hunk {
-                                                        let first_hunk_start =
-                                                            first_hunk.multi_buffer_range().start;
-                                                        editor.change_selections(
-                                                            Default::default(),
-                                                            window,
-                                                            cx,
-                                                            |selections| {
-                                                                selections.select_anchor_ranges([
-                                                                    first_hunk_start
-                                                                        ..first_hunk_start,
-                                                                ]);
-                                                            },
-                                                        )
-                                                    }
-                                                })
-                                                .log_err();
-                                        }
-                                        anyhow::Ok(())
-                                    })
-                                    .detach_and_log_err(cx);
-                            }
-                        })
-                        .ok();
-                }
-            })
-            .into_any_element();
-
-        let codeblock_header_bg = cx
-            .theme()
-            .colors()
-            .element_background
-            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
-
-        let codeblock_header = h_flex()
-            .flex_none()
-            .p_1()
-            .gap_1()
-            .justify_between()
-            .rounded_t_md()
-            .when(error_message.is_none(), |header| {
-                header.bg(codeblock_header_bg)
-            })
-            .child(path_label_button)
-            .when(should_show_loading, |header| {
-                header.pr_1p5().child(
-                    Icon::new(IconName::ArrowCircle)
-                        .size(IconSize::XSmall)
-                        .color(Color::Info)
-                        .with_rotate_animation(2),
-                )
-            })
-            .when_some(error_message, |header, error_message| {
-                header.child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Icon::new(IconName::Close)
-                                .size(IconSize::Small)
-                                .color(Color::Error),
-                        )
-                        .child(
-                            Disclosure::new(
-                                ("edit-file-error-disclosure", self.editor.entity_id()),
-                                self.error_expanded.is_some(),
-                            )
-                            .opened_icon(IconName::ChevronUp)
-                            .closed_icon(IconName::ChevronDown)
-                            .on_click(cx.listener({
-                                let error_message = error_message.clone();
-
-                                move |this, _event, _window, cx| {
-                                    if this.error_expanded.is_some() {
-                                        this.error_expanded.take();
-                                    } else {
-                                        this.error_expanded = Some(cx.new(|cx| {
-                                            Markdown::new(error_message.clone(), None, None, cx)
-                                        }))
-                                    }
-                                    cx.notify();
-                                }
-                            })),
-                        ),
-                )
-            })
-            .when(error_message.is_none() && !self.is_loading(), |header| {
-                header.child(
-                    Disclosure::new(
-                        ("edit-file-disclosure", self.editor.entity_id()),
-                        self.preview_expanded,
-                    )
-                    .opened_icon(IconName::ChevronUp)
-                    .closed_icon(IconName::ChevronDown)
-                    .on_click(cx.listener(
-                        move |this, _event, _window, _cx| {
-                            this.preview_expanded = !this.preview_expanded;
-                        },
-                    )),
-                )
-            });
-
-        let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
-            let line_height = editor
-                .style()
-                .map(|style| style.text.line_height_in_pixels(window.rem_size()))
-                .unwrap_or_default();
-
-            editor.set_text_style_refinement(TextStyleRefinement {
-                font_size: Some(
-                    TextSize::Small
-                        .rems(cx)
-                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
-                        .into(),
-                ),
-                ..TextStyleRefinement::default()
-            });
-            let element = editor.render(window, cx);
-            (element.into_any_element(), line_height)
-        });
-
-        let border_color = cx.theme().colors().border.opacity(0.6);
-
-        let waiting_for_diff = {
-            let styles = [
-                ("w_4_5", (0.1, 0.85), 2000),
-                ("w_1_4", (0.2, 0.75), 2200),
-                ("w_2_4", (0.15, 0.64), 1900),
-                ("w_3_5", (0.25, 0.72), 2300),
-                ("w_2_5", (0.3, 0.56), 1800),
-            ];
-
-            let mut container = v_flex()
-                .p_3()
-                .gap_1()
-                .border_t_1()
-                .rounded_b_md()
-                .border_color(border_color)
-                .bg(cx.theme().colors().editor_background);
-
-            for (width_method, pulse_range, duration_ms) in styles.iter() {
-                let (min_opacity, max_opacity) = *pulse_range;
-                let placeholder = match *width_method {
-                    "w_4_5" => div().w_3_4(),
-                    "w_1_4" => div().w_1_4(),
-                    "w_2_4" => div().w_2_4(),
-                    "w_3_5" => div().w_3_5(),
-                    "w_2_5" => div().w_2_5(),
-                    _ => div().w_1_2(),
-                }
-                .id("loading_div")
-                .h_1()
-                .rounded_full()
-                .bg(cx.theme().colors().element_active)
-                .with_animation(
-                    "loading_pulsate",
-                    Animation::new(Duration::from_millis(*duration_ms))
-                        .repeat()
-                        .with_easing(pulsating_between(min_opacity, max_opacity)),
-                    |label, delta| label.opacity(delta),
-                );
-
-                container = container.child(placeholder);
-            }
-
-            container
-        };
-
-        v_flex()
-            .mb_2()
-            .border_1()
-            .when(error_message.is_some(), |card| card.border_dashed())
-            .border_color(border_color)
-            .rounded_md()
-            .overflow_hidden()
-            .child(codeblock_header)
-            .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
-                card.child(
-                    v_flex()
-                        .p_2()
-                        .gap_1()
-                        .border_t_1()
-                        .border_dashed()
-                        .border_color(border_color)
-                        .bg(cx.theme().colors().editor_background)
-                        .rounded_b_md()
-                        .child(
-                            Label::new("Error")
-                                .size(LabelSize::XSmall)
-                                .color(Color::Error),
-                        )
-                        .child(
-                            div()
-                                .rounded_md()
-                                .text_ui_sm(cx)
-                                .bg(cx.theme().colors().editor_background)
-                                .child(MarkdownElement::new(
-                                    error_markdown.clone(),
-                                    markdown_style(window, cx),
-                                )),
-                        ),
-                )
-            })
-            .when(self.is_loading() && error_message.is_none(), |card| {
-                card.child(waiting_for_diff)
-            })
-            .when(self.preview_expanded && !self.is_loading(), |card| {
-                let editor_view = v_flex()
-                    .relative()
-                    .h_full()
-                    .when(!self.full_height_expanded, |editor_container| {
-                        editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0))
-                    })
-                    .overflow_hidden()
-                    .border_t_1()
-                    .border_color(border_color)
-                    .bg(cx.theme().colors().editor_background)
-                    .child(editor);
-
-                card.child(
-                    ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id())
-                        .with_total_lines(self.total_lines.unwrap_or(0) as usize)
-                        .toggle_state(self.full_height_expanded)
-                        .with_collapsed_fade()
-                        .on_toggle({
-                            let this = cx.entity().downgrade();
-                            move |is_expanded, _window, cx| {
-                                if let Some(this) = this.upgrade() {
-                                    this.update(cx, |this, _cx| {
-                                        this.full_height_expanded = is_expanded;
-                                    });
-                                }
-                            }
-                        }),
-                )
-            })
-    }
-}
-
-fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let ui_font_size = TextSize::Default.rems(cx);
-    let mut text_style = window.text_style();
-
-    text_style.refine(&TextStyleRefinement {
-        font_family: Some(theme_settings.ui_font.family.clone()),
-        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
-        font_features: Some(theme_settings.ui_font.features.clone()),
-        font_size: Some(ui_font_size.into()),
-        color: Some(cx.theme().colors().text),
-        ..Default::default()
-    });
-
-    MarkdownStyle {
-        base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().colors().element_selection_background,
-        ..Default::default()
-    }
-}
-
-async fn build_buffer(
-    mut text: String,
-    path: Arc<Path>,
-    language_registry: &Arc<language::LanguageRegistry>,
-    cx: &mut AsyncApp,
-) -> Result<Entity<Buffer>> {
-    let line_ending = LineEnding::detect(&text);
-    LineEnding::normalize(&mut text);
-    let text = Rope::from(text);
-    let language = cx
-        .update(|_cx| language_registry.language_for_file_path(&path))?
-        .await
-        .ok();
-    let buffer = cx.new(|cx| {
-        let buffer = TextBuffer::new_normalized(
-            0,
-            cx.entity_id().as_non_zero_u64().into(),
-            line_ending,
-            text,
-        );
-        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
-        buffer.set_language(language, cx);
-        buffer
-    })?;
-    Ok(buffer)
-}
-
-async fn build_buffer_diff(
-    old_text: Arc<String>,
-    buffer: &Entity<Buffer>,
-    language_registry: &Arc<LanguageRegistry>,
-    cx: &mut AsyncApp,
-) -> Result<Entity<BufferDiff>> {
-    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
-
-    let old_text_rope = cx
-        .background_spawn({
-            let old_text = old_text.clone();
-            async move { Rope::from(old_text.as_str()) }
-        })
-        .await;
-    let base_buffer = cx
-        .update(|cx| {
-            Buffer::build_snapshot(
-                old_text_rope,
-                buffer.language().cloned(),
-                Some(language_registry.clone()),
-                cx,
-            )
-        })?
-        .await;
-
-    let diff_snapshot = cx
-        .update(|cx| {
-            BufferDiffSnapshot::new_with_base_buffer(
-                buffer.text.clone(),
-                Some(old_text),
-                base_buffer,
-                cx,
-            )
-        })?
-        .await;
-
-    let secondary_diff = cx.new(|cx| {
-        let mut diff = BufferDiff::new(&buffer, cx);
-        diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
-        diff
-    })?;
-
-    cx.new(|cx| {
-        let mut diff = BufferDiff::new(&buffer.text, cx);
-        diff.set_snapshot(diff_snapshot, &buffer, cx);
-        diff.set_secondary_diff(secondary_diff);
-        diff
-    })
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use ::fs::Fs;
-    use client::TelemetrySettings;
-    use encodings::Encoding;
-    use encodings::UTF_8;
-    use gpui::{TestAppContext, UpdateGlobal};
-    use language_model::fake_provider::FakeLanguageModel;
-    use serde_json::json;
-    use settings::SettingsStore;
-    use std::fs;
-    use util::path;
-
-    #[gpui::test]
-    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/root", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let result = cx
-            .update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Some edit".into(),
-                    path: "root/nonexistent_file.txt".into(),
-                    mode: EditFileMode::Edit,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(
-            result.unwrap_err().to_string(),
-            "Can't edit file: path not found"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
-        let mode = &EditFileMode::Create;
-
-        let result = test_resolve_path(mode, "root/new.txt", cx);
-        assert_resolved_path_eq(result.await, "new.txt");
-
-        let result = test_resolve_path(mode, "new.txt", cx);
-        assert_resolved_path_eq(result.await, "new.txt");
-
-        let result = test_resolve_path(mode, "dir/new.txt", cx);
-        assert_resolved_path_eq(result.await, "dir/new.txt");
-
-        let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
-        assert_eq!(
-            result.await.unwrap_err().to_string(),
-            "Can't create file: file already exists"
-        );
-
-        let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
-        assert_eq!(
-            result.await.unwrap_err().to_string(),
-            "Can't create file: parent directory doesn't exist"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
-        let mode = &EditFileMode::Edit;
-
-        let path_with_root = "root/dir/subdir/existing.txt";
-        let path_without_root = "dir/subdir/existing.txt";
-        let result = test_resolve_path(mode, path_with_root, cx);
-        assert_resolved_path_eq(result.await, path_without_root);
-
-        let result = test_resolve_path(mode, path_without_root, cx);
-        assert_resolved_path_eq(result.await, path_without_root);
-
-        let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
-        assert_eq!(
-            result.await.unwrap_err().to_string(),
-            "Can't edit file: path not found"
-        );
-
-        let result = test_resolve_path(mode, "root/dir", cx);
-        assert_eq!(
-            result.await.unwrap_err().to_string(),
-            "Can't edit file: path is a directory"
-        );
-    }
-
-    async fn test_resolve_path(
-        mode: &EditFileMode,
-        path: &str,
-        cx: &mut TestAppContext,
-    ) -> anyhow::Result<ProjectPath> {
-        init_test(cx);
-
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/root",
-            json!({
-                "dir": {
-                    "subdir": {
-                        "existing.txt": "hello"
-                    }
-                }
-            }),
-        )
-        .await;
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-
-        let input = EditFileToolInput {
-            display_description: "Some edit".into(),
-            path: path.into(),
-            mode: mode.clone(),
-        };
-
-        cx.update(|cx| resolve_path(&input, project, cx))
-    }
-
-    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
-        let actual = path
-            .expect("Should return valid path")
-            .path
-            .to_str()
-            .unwrap()
-            .replace("\\", "/"); // Naive Windows paths normalization
-        assert_eq!(actual, expected);
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_path() {
-        let input = json!({
-            "path": "src/main.rs",
-            "display_description": "",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_description() {
-        let input = json!({
-            "path": "",
-            "display_description": "Fix error handling",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            "Fix error handling",
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_path_and_description() {
-        let input = json!({
-            "path": "src/main.rs",
-            "display_description": "Fix error handling",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            "Fix error handling",
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_no_path_or_description() {
-        let input = json!({
-            "path": "",
-            "display_description": "",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            DEFAULT_UI_TEXT,
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_null() {
-        let input = serde_json::Value::Null;
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            DEFAULT_UI_TEXT,
-        );
-    }
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            language::init(cx);
-            TelemetrySettings::register(cx);
-            agent_settings::AgentSettings::register(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
-        cx.update(|cx| {
-            // Set custom data directory (config will be under data_dir/config)
-            paths::set_custom_data_dir(data_dir.to_str().unwrap());
-
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            language::init(cx);
-            TelemetrySettings::register(cx);
-            agent_settings::AgentSettings::register(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_format_on_save(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/root", json!({"src": {}})).await;
-
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-
-        // Set up a Rust language with LSP formatting support
-        let rust_language = Arc::new(language::Language::new(
-            language::LanguageConfig {
-                name: "Rust".into(),
-                matcher: language::LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            None,
-        ));
-
-        // Register the language and fake LSP
-        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
-        language_registry.add(rust_language);
-
-        let mut fake_language_servers = language_registry.register_fake_lsp(
-            "Rust",
-            language::FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-        );
-
-        // Create the file
-        fs.save(
-            path!("/root/src/main.rs").as_ref(),
-            &"initial content".into(),
-            language::LineEnding::Unix,
-            Encoding::new(UTF_8),
-        )
-        .await
-        .unwrap();
-
-        // Open the buffer to trigger LSP initialization
-        let buffer = project
-            .update(cx, |project, cx| {
-                project.open_local_buffer(path!("/root/src/main.rs"), cx)
-            })
-            .await
-            .unwrap();
-
-        // Register the buffer with language servers
-        let _handle = project.update(cx, |project, cx| {
-            project.register_buffer_with_language_servers(&buffer, cx)
-        });
-
-        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
-        const FORMATTED_CONTENT: &str =
-            "This file was formatted by the fake formatter in the test.\n";
-
-        // Get the fake language server and set up formatting handler
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
-            |_, _| async move {
-                Ok(Some(vec![lsp::TextEdit {
-                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
-                    new_text: FORMATTED_CONTENT.to_string(),
-                }]))
-            }
-        });
-
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // First, test with format_on_save enabled
-        cx.update(|cx| {
-            SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::On);
-                        settings.defaults.formatter =
-                            Some(language::language_settings::SelectedFormatter::Auto);
-                    },
-                );
-            });
-        });
-
-        // Have the model stream unformatted content
-        let edit_result = {
-            let edit_task = cx.update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Create main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // Stream the unformatted content
-            cx.executor().run_until_parked();
-            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
-            model.end_last_completion_stream();
-
-            edit_task.await
-        };
-        assert!(edit_result.is_ok());
-
-        // Wait for any async operations (e.g. formatting) to complete
-        cx.executor().run_until_parked();
-
-        // Read the file to verify it was formatted automatically
-        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
-        assert_eq!(
-            // Ignore carriage returns on Windows
-            new_content.replace("\r\n", "\n"),
-            FORMATTED_CONTENT,
-            "Code should be formatted when format_on_save is enabled"
-        );
-
-        let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
-
-        assert_eq!(
-            stale_buffer_count, 0,
-            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
-             This causes the agent to think the file was modified externally when it was just formatted.",
-            stale_buffer_count
-        );
-
-        // Next, test with format_on_save disabled
-        cx.update(|cx| {
-            SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
-                    },
-                );
-            });
-        });
-
-        // Stream unformatted edits again
-        let edit_result = {
-            let edit_task = cx.update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Update main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // Stream the unformatted content
-            cx.executor().run_until_parked();
-            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
-            model.end_last_completion_stream();
-
-            edit_task.await
-        };
-        assert!(edit_result.is_ok());
-
-        // Wait for any async operations (e.g. formatting) to complete
-        cx.executor().run_until_parked();
-
-        // Verify the file was not formatted
-        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
-        assert_eq!(
-            // Ignore carriage returns on Windows
-            new_content.replace("\r\n", "\n"),
-            UNFORMATTED_CONTENT,
-            "Code should not be formatted when format_on_save is disabled"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/root", json!({"src": {}})).await;
-
-        // Create a simple file with trailing whitespace
-        fs.save(
-            path!("/root/src/main.rs").as_ref(),
-            &"initial content".into(),
-            language::LineEnding::Unix,
-            Encoding::default(),
-        )
-        .await
-        .unwrap();
-
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // First, test with remove_trailing_whitespace_on_save enabled
-        cx.update(|cx| {
-            SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
-                    },
-                );
-            });
-        });
-
-        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
-            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
-
-        // Have the model stream content that contains trailing whitespace
-        let edit_result = {
-            let edit_task = cx.update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Create main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // Stream the content with trailing whitespace
-            cx.executor().run_until_parked();
-            model.send_last_completion_stream_text_chunk(
-                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
-            );
-            model.end_last_completion_stream();
-
-            edit_task.await
-        };
-        assert!(edit_result.is_ok());
-
-        // Wait for any async operations (e.g. formatting) to complete
-        cx.executor().run_until_parked();
-
-        // Read the file to verify trailing whitespace was removed automatically
-        assert_eq!(
-            // Ignore carriage returns on Windows
-            fs.load(path!("/root/src/main.rs").as_ref())
-                .await
-                .unwrap()
-                .replace("\r\n", "\n"),
-            "fn main() {\n    println!(\"Hello!\");\n}\n",
-            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
-        );
-
-        // Next, test with remove_trailing_whitespace_on_save disabled
-        cx.update(|cx| {
-            SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
-                    },
-                );
-            });
-        });
-
-        // Stream edits again with trailing whitespace
-        let edit_result = {
-            let edit_task = cx.update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Update main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // Stream the content with trailing whitespace
-            cx.executor().run_until_parked();
-            model.send_last_completion_stream_text_chunk(
-                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
-            );
-            model.end_last_completion_stream();
-
-            edit_task.await
-        };
-        assert!(edit_result.is_ok());
-
-        // Wait for any async operations (e.g. formatting) to complete
-        cx.executor().run_until_parked();
-
-        // Verify the file still has trailing whitespace
-        // Read the file again - it should still have trailing whitespace
-        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
-        assert_eq!(
-            // Ignore carriage returns on Windows
-            final_content.replace("\r\n", "\n"),
-            CONTENT_WITH_TRAILING_WHITESPACE,
-            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/root", json!({})).await;
-
-        // Test 1: Path with .zed component should require confirmation
-        let input_with_zed = json!({
-            "display_description": "Edit settings",
-            "path": ".zed/settings.json",
-            "mode": "edit"
-        });
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_with_zed, &project, cx),
-                "Path with .zed component should require confirmation"
-            );
-        });
-
-        // Test 2: Absolute path should require confirmation
-        let input_absolute = json!({
-            "display_description": "Edit file",
-            "path": "/etc/hosts",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_absolute, &project, cx),
-                "Absolute path should require confirmation"
-            );
-        });
-
-        // Test 3: Relative path without .zed should not require confirmation
-        let input_relative = json!({
-            "display_description": "Edit file",
-            "path": "root/src/main.rs",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                !tool.needs_confirmation(&input_relative, &project, cx),
-                "Relative path without .zed should not require confirmation"
-            );
-        });
-
-        // Test 4: Path with .zed in the middle should require confirmation
-        let input_zed_middle = json!({
-            "display_description": "Edit settings",
-            "path": "root/.zed/tasks.json",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_zed_middle, &project, cx),
-                "Path with .zed in any component should require confirmation"
-            );
-        });
-
-        // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
-        cx.update(|cx| {
-            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
-            settings.always_allow_tool_actions = true;
-            agent_settings::AgentSettings::override_global(settings, cx);
-
-            assert!(
-                !tool.needs_confirmation(&input_with_zed, &project, cx),
-                "When always_allow_tool_actions is true, no confirmation should be needed"
-            );
-            assert!(
-                !tool.needs_confirmation(&input_absolute, &project, cx),
-                "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
-            );
-        });
-    }
-
-    #[gpui::test]
-    async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
-        // Set up a custom config directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-
-        // Test ui_text shows context for various paths
-        let test_cases = vec![
-            (
-                json!({
-                    "display_description": "Update config",
-                    "path": ".zed/settings.json",
-                    "mode": "edit"
-                }),
-                "Update config (local settings)",
-                ".zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Fix bug",
-                    "path": "src/.zed/local.json",
-                    "mode": "edit"
-                }),
-                "Fix bug (local settings)",
-                "Nested .zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Update readme",
-                    "path": "README.md",
-                    "mode": "edit"
-                }),
-                "Update readme",
-                "Normal path should not show additional context",
-            ),
-            (
-                json!({
-                    "display_description": "Edit config",
-                    "path": "config.zed",
-                    "mode": "edit"
-                }),
-                "Edit config",
-                ".zed as extension should not show context",
-            ),
-        ];
-
-        for (input, expected_text, description) in test_cases {
-            cx.update(|_cx| {
-                let ui_text = tool.ui_text(&input);
-                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-
-        // Create a project in /project directory
-        fs.insert_tree("/project", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test file outside project requires confirmation
-        let input_outside = json!({
-            "display_description": "Edit file",
-            "path": "/outside/file.txt",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_outside, &project, cx),
-                "File outside project should require confirmation"
-            );
-        });
-
-        // Test file inside project doesn't require confirmation
-        let input_inside = json!({
-            "display_description": "Edit file",
-            "path": "project/file.txt",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                !tool.needs_confirmation(&input_inside, &project, cx),
-                "File inside project should not require confirmation"
-            );
-        });
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
-        // Set up a custom data directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/home/user/myproject", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
-
-        // Get the actual local settings folder name
-        let local_settings_folder = paths::local_settings_folder_relative_path();
-
-        // Test various config path patterns
-        let test_cases = vec![
-            (
-                format!("{}/settings.json", local_settings_folder.display()),
-                true,
-                "Top-level local settings file".to_string(),
-            ),
-            (
-                format!(
-                    "myproject/{}/settings.json",
-                    local_settings_folder.display()
-                ),
-                true,
-                "Local settings in project path".to_string(),
-            ),
-            (
-                format!("src/{}/config.toml", local_settings_folder.display()),
-                true,
-                "Local settings in subdirectory".to_string(),
-            ),
-            (
-                ".zed.backup/file.txt".to_string(),
-                true,
-                ".zed.backup is outside project".to_string(),
-            ),
-            (
-                "my.zed/file.txt".to_string(),
-                true,
-                "my.zed is outside project".to_string(),
-            ),
-            (
-                "myproject/src/file.zed".to_string(),
-                false,
-                ".zed as file extension".to_string(),
-            ),
-            (
-                "myproject/normal/path/file.rs".to_string(),
-                false,
-                "Normal file without config paths".to_string(),
-            ),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
-        // Set up a custom data directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-
-        // Create test files in the global config directory
-        let global_config_dir = paths::config_dir();
-        fs::create_dir_all(&global_config_dir).unwrap();
-        let global_settings_path = global_config_dir.join("settings.json");
-        fs::write(&global_settings_path, "{}").unwrap();
-
-        fs.insert_tree("/project", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test global config paths
-        let test_cases = vec![
-            (
-                global_settings_path.to_str().unwrap().to_string(),
-                true,
-                "Global settings file should require confirmation",
-            ),
-            (
-                global_config_dir
-                    .join("keymap.json")
-                    .to_str()
-                    .unwrap()
-                    .to_string(),
-                true,
-                "Global keymap file should require confirmation",
-            ),
-            (
-                "project/normal_file.rs".to_string(),
-                false,
-                "Normal project file should not require confirmation",
-            ),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {}",
-                    description
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-
-        // Create multiple worktree directories
-        fs.insert_tree(
-            "/workspace/frontend",
-            json!({
-                "src": {
-                    "main.js": "console.log('frontend');"
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/workspace/backend",
-            json!({
-                "src": {
-                    "main.rs": "fn main() {}"
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/workspace/shared",
-            json!({
-                ".zed": {
-                    "settings.json": "{}"
-                }
-            }),
-        )
-        .await;
-
-        // Create project with multiple worktrees
-        let project = Project::test(
-            fs.clone(),
-            [
-                path!("/workspace/frontend").as_ref(),
-                path!("/workspace/backend").as_ref(),
-                path!("/workspace/shared").as_ref(),
-            ],
-            cx,
-        )
-        .await;
-
-        // Test files in different worktrees
-        let test_cases = vec![
-            ("frontend/src/main.js", false, "File in first worktree"),
-            ("backend/src/main.rs", false, "File in second worktree"),
-            (
-                "shared/.zed/settings.json",
-                true,
-                ".zed file in third worktree",
-            ),
-            ("/etc/hosts", true, "Absolute path outside all worktrees"),
-            (
-                "../outside/file.txt",
-                true,
-                "Relative path outside worktrees",
-            ),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                ".zed": {
-                    "settings.json": "{}"
-                },
-                "src": {
-                    ".zed": {
-                        "local.json": "{}"
-                    }
-                }
-            }),
-        )
-        .await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test edge cases
-        let test_cases = vec![
-            // Empty path - find_project_path returns Some for empty paths
-            ("", false, "Empty path is treated as project root"),
-            // Root directory
-            ("/", true, "Root directory should be outside project"),
-            // Parent directory references - find_project_path resolves these
-            (
-                "project/../other",
-                false,
-                "Path with .. is resolved by find_project_path",
-            ),
-            (
-                "project/./src/file.rs",
-                false,
-                "Path with . should work normally",
-            ),
-            // Windows-style paths (if on Windows)
-            #[cfg(target_os = "windows")]
-            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
-            #[cfg(target_os = "windows")]
-            ("project\\src\\main.rs", false, "Windows-style project path"),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-
-        // Test UI text for various scenarios
-        let test_cases = vec![
-            (
-                json!({
-                    "display_description": "Update config",
-                    "path": ".zed/settings.json",
-                    "mode": "edit"
-                }),
-                "Update config (local settings)",
-                ".zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Fix bug",
-                    "path": "src/.zed/local.json",
-                    "mode": "edit"
-                }),
-                "Fix bug (local settings)",
-                "Nested .zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Update readme",
-                    "path": "README.md",
-                    "mode": "edit"
-                }),
-                "Update readme",
-                "Normal path should not show additional context",
-            ),
-            (
-                json!({
-                    "display_description": "Edit config",
-                    "path": "config.zed",
-                    "mode": "edit"
-                }),
-                "Edit config",
-                ".zed as extension should not show context",
-            ),
-        ];
-
-        for (input, expected_text, description) in test_cases {
-            cx.update(|_cx| {
-                let ui_text = tool.ui_text(&input);
-                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                "existing.txt": "content",
-                ".zed": {
-                    "settings.json": "{}"
-                }
-            }),
-        )
-        .await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test different EditFileMode values
-        let modes = vec![
-            EditFileMode::Edit,
-            EditFileMode::Create,
-            EditFileMode::Overwrite,
-        ];
-
-        for mode in modes {
-            // Test .zed path with different modes
-            let input_zed = json!({
-                "display_description": "Edit settings",
-                "path": "project/.zed/settings.json",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    tool.needs_confirmation(&input_zed, &project, cx),
-                    ".zed path should require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-
-            // Test outside path with different modes
-            let input_outside = json!({
-                "display_description": "Edit file",
-                "path": "/outside/file.txt",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    tool.needs_confirmation(&input_outside, &project, cx),
-                    "Outside path should require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-
-            // Test normal path with different modes
-            let input_normal = json!({
-                "display_description": "Edit file",
-                "path": "project/normal.txt",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    !tool.needs_confirmation(&input_normal, &project, cx),
-                    "Normal path should not require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
-        // Set up with custom directories for deterministic testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/project", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Enable always_allow_tool_actions
-        cx.update(|cx| {
-            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
-            settings.always_allow_tool_actions = true;
-            agent_settings::AgentSettings::override_global(settings, cx);
-        });
-
-        // Test that all paths that normally require confirmation are bypassed
-        let global_settings_path = paths::config_dir().join("settings.json");
-        fs::create_dir_all(paths::config_dir()).unwrap();
-        fs::write(&global_settings_path, "{}").unwrap();
-
-        let test_cases = vec![
-            ".zed/settings.json",
-            "project/.zed/config.toml",
-            global_settings_path.to_str().unwrap(),
-            "/etc/hosts",
-            "/absolute/path/file.txt",
-            "../outside/project.txt",
-        ];
-
-        for path in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert!(
-                    !tool.needs_confirmation(&input, &project, cx),
-                    "Path {} should not require confirmation when always_allow_tool_actions is true",
-                    path
-                );
-            });
-        }
-
-        // Disable always_allow_tool_actions and verify confirmation is required again
-        cx.update(|cx| {
-            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
-            settings.always_allow_tool_actions = false;
-            agent_settings::AgentSettings::override_global(settings, cx);
-        });
-
-        // Verify .zed path requires confirmation again
-        let input = json!({
-            "display_description": "Edit file",
-            "path": ".zed/settings.json",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input, &project, cx),
-                ".zed path should require confirmation when always_allow_tool_actions is false"
-            );
-        });
-    }
-}

crates/edit_prediction_context/src/syntax_index.rs 🔗

@@ -524,6 +524,8 @@ impl SyntaxIndex {
 
         let snapshot_task = worktree.update(cx, |worktree, cx| {
             let load_task = worktree.load_file(&project_path.path, None, false, true, None, cx);
+            let worktree_abs_path = worktree.abs_path();
+
             cx.spawn(async move |_this, cx| {
                 let loaded_file = load_task.await?;
                 let language = language.await?;

crates/editor/src/editor_settings.rs 🔗

@@ -64,18 +64,6 @@ pub struct Jupyter {
     pub enabled: bool,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct StatusBar {
-    /// Whether to display the active language button in the status bar.
-    ///
-    /// Default: true
-    pub active_language_button: bool,
-    /// Whether to show the cursor position button in the status bar.
-    ///
-    /// Default: true
-    pub cursor_position_button: bool,
-}
-
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Toolbar {
     pub breadcrumbs: bool,

crates/encodings_ui/Cargo.toml 🔗

@@ -18,7 +18,6 @@ settings.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 zed_actions.workspace = true
 
 

crates/encodings_ui/src/lib.rs 🔗

@@ -75,7 +75,7 @@ impl Render for EncodingIndicator {
                             let weak_workspace = workspace.weak_handle();
 
                             if let Some(path) = buffer.read(cx).file() {
-                                let path = path.clone().path().to_path_buf();
+                                let path = path.clone().path().to_rel_path_buf();
                                 workspace.toggle_modal(window, cx, |window, cx| {
                                     let selector = EncodingSelector::new(
                                         window,
@@ -83,7 +83,7 @@ impl Render for EncodingIndicator {
                                         Action::Save,
                                         Some(buffer.downgrade()),
                                         weak_workspace,
-                                        Some(path),
+                                        Some(path.as_std_path().to_path_buf()),
                                     );
                                     selector
                                 });

crates/git_ui/src/file_diff_view.rs 🔗

@@ -363,7 +363,6 @@ mod tests {
     use language::Rope;
     use project::{FakeFs, Fs, Project};
     use settings::SettingsStore;
-    use project::{FakeFs, Fs, Project, encodings::EncodingWrapper};
     use std::path::PathBuf;
     use unindent::unindent;
     use util::path;

crates/language/Cargo.toml 🔗

@@ -69,7 +69,6 @@ unicase = "2.6"
 util.workspace = true
 watch.workspace = true
 zlog.workspace = true
-diffy = "0.4.2"
 encoding = "0.2.33"
 
 [dev-dependencies]

crates/project/src/buffer_store.rs 🔗

@@ -8,8 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
 use client::Client;
 use collections::{HashMap, HashSet, hash_map};
 use encodings::Encoding;
-use fs::Fs;
-use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared};
+use futures::{Future, FutureExt as _, channel::oneshot, future::Shared};
 use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
 };
@@ -27,7 +26,7 @@ use rpc::{
 use smol::channel::Receiver;
 use std::{io, pin::pin, sync::Arc, time::Instant};
 use text::{BufferId, ReplicaId};
-use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath};
+use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath};
 use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId};
 
 /// A set of open buffers.
@@ -398,7 +397,7 @@ impl LocalBufferStore {
         }
 
         let save = worktree.update(cx, |worktree, cx| {
-            worktree.write_file(path.as_ref(), text, line_ending, cx, (*encoding).clone())
+            worktree.write_file(path.clone(), text, line_ending, cx, (*encoding).clone())
         });
 
         cx.spawn(async move |this, cx| {
@@ -633,8 +632,6 @@ impl LocalBufferStore {
         detect_utf16: bool,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
-        println!("{:?}", encoding);
-
         let load_buffer = worktree.update(cx, |worktree, cx| {
             let reservation = cx.reserve_entity();
             let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
@@ -653,7 +650,7 @@ impl LocalBufferStore {
 
                 let buffer = cx.insert_entity(reservation, |_| {
                     Buffer::build(
-                        text::Buffer::new(0, buffer_id, loaded_file.text),
+                        text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded_file.text),
                         Some(loaded_file.file),
                         Capability::ReadWrite,
                     )

crates/project/src/project.rs 🔗

@@ -27,7 +27,7 @@ mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
 
-use encodings::EncodingOptions;
+use encodings::{Encoding, EncodingOptions};
 pub use environment::ProjectEnvironmentEvent;
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
@@ -5408,7 +5408,9 @@ impl Project {
         };
         cx.spawn(async move |cx| {
             let file = worktree
-                .update(cx, |worktree, cx| worktree.load_file(&rel_path, cx))?
+                .update(cx, |worktree, cx| {
+                    worktree.load_file(&rel_path, None, false, true, None, cx)
+                })?
                 .await
                 .context("Failed to load settings file")?;
 
@@ -5423,6 +5425,11 @@ impl Project {
                         Rope::from_str(&new_text, cx.background_executor()),
                         line_ending,
                         cx,
+                        Encoding::default(),
+                        new_text.into(),
+                        line_ending,
+                        cx,
+                        Encoding::default(),
                     )
                 })?
                 .await

crates/project/src/project_search.rs 🔗

@@ -0,0 +1,754 @@
+use std::{
+    io::{BufRead, BufReader},
+    path::Path,
+    pin::pin,
+    sync::{
+        Arc,
+        atomic::{AtomicUsize, Ordering},
+    },
+};
+
+use anyhow::Context;
+use collections::HashSet;
+use fs::Fs;
+use futures::{SinkExt, StreamExt, select_biased, stream::FuturesOrdered};
+use gpui::{App, AppContext, AsyncApp, Entity, Task};
+use language::{Buffer, BufferSnapshot};
+use parking_lot::Mutex;
+use postage::oneshot;
+use rpc::{AnyProtoClient, proto};
+use smol::{
+    channel::{Receiver, Sender, bounded, unbounded},
+    future::FutureExt,
+};
+
+use text::BufferId;
+use util::{ResultExt, maybe, paths::compare_rel_paths};
+use worktree::{Entry, ProjectEntryId, Snapshot, Worktree};
+
+use crate::{
+    Project, ProjectItem, ProjectPath, RemotelyCreatedModels,
+    buffer_store::BufferStore,
+    search::{SearchQuery, SearchResult},
+    worktree_store::WorktreeStore,
+};
+
+pub struct Search {
+    buffer_store: Entity<BufferStore>,
+    worktree_store: Entity<WorktreeStore>,
+    limit: usize,
+    kind: SearchKind,
+}
+
+/// Represents search setup, before it is actually kicked off with Search::into_results
+enum SearchKind {
+    /// Search for candidates by inspecting file contents on file system, avoiding loading the buffer unless we know that a given file contains a match.
+    Local {
+        fs: Arc<dyn Fs>,
+        worktrees: Vec<Entity<Worktree>>,
+    },
+    /// Query remote host for candidates. As of writing, the host runs a local search in "buffers with matches only" mode.
+    Remote {
+        client: AnyProtoClient,
+        remote_id: u64,
+        models: Arc<Mutex<RemotelyCreatedModels>>,
+    },
+    /// Run search against a known set of candidates. Even when working with a remote host, this won't round-trip to host.
+    OpenBuffersOnly,
+}
+
+/// Represents results of project search and allows one to either obtain match positions OR
+/// just the handles to buffers that may match the search. Grabbing the handles is cheaper than obtaining full match positions, because in that case we'll look for
+/// at most one match in each file.
+#[must_use]
+pub struct SearchResultsHandle {
+    results: Receiver<SearchResult>,
+    matching_buffers: Receiver<Entity<Buffer>>,
+    trigger_search: Box<dyn FnOnce(&mut App) -> Task<()> + Send + Sync>,
+}
+
+impl SearchResultsHandle {
+    pub fn results(self, cx: &mut App) -> Receiver<SearchResult> {
+        (self.trigger_search)(cx).detach();
+        self.results
+    }
+    pub fn matching_buffers(self, cx: &mut App) -> Receiver<Entity<Buffer>> {
+        (self.trigger_search)(cx).detach();
+        self.matching_buffers
+    }
+}
+
+#[derive(Clone)]
+enum FindSearchCandidates {
+    Local {
+        fs: Arc<dyn Fs>,
+        /// Start off with all paths in project and filter them based on:
+        /// - Include filters
+        /// - Exclude filters
+        /// - Only open buffers
+        /// - Scan ignored files
+        /// Put another way: filter out files that can't match (without looking at file contents)
+        input_paths_rx: Receiver<InputPath>,
+        /// After that, if the buffer is not yet loaded, we'll figure out if it contains at least one match
+        /// based on disk contents of a buffer. This step is not performed for buffers we already have in memory.
+        confirm_contents_will_match_tx: Sender<MatchingEntry>,
+        confirm_contents_will_match_rx: Receiver<MatchingEntry>,
+        /// Of those that contain at least one match (or are already in memory), look for rest of matches (and figure out their ranges).
+        /// But wait - first, we need to go back to the main thread to open a buffer (& create an entity for it).
+        get_buffer_for_full_scan_tx: Sender<ProjectPath>,
+    },
+    Remote,
+    OpenBuffersOnly,
+}
+
+impl Search {
+    pub fn local(
+        fs: Arc<dyn Fs>,
+        buffer_store: Entity<BufferStore>,
+        worktree_store: Entity<WorktreeStore>,
+        limit: usize,
+        cx: &mut App,
+    ) -> Self {
+        let worktrees = worktree_store.read(cx).visible_worktrees(cx).collect();
+        Self {
+            kind: SearchKind::Local { fs, worktrees },
+            buffer_store,
+            worktree_store,
+            limit,
+        }
+    }
+
+    pub(crate) fn remote(
+        buffer_store: Entity<BufferStore>,
+        worktree_store: Entity<WorktreeStore>,
+        limit: usize,
+        client_state: (AnyProtoClient, u64, Arc<Mutex<RemotelyCreatedModels>>),
+    ) -> Self {
+        Self {
+            kind: SearchKind::Remote {
+                client: client_state.0,
+                remote_id: client_state.1,
+                models: client_state.2,
+            },
+            buffer_store,
+            worktree_store,
+            limit,
+        }
+    }
+    pub(crate) fn open_buffers_only(
+        buffer_store: Entity<BufferStore>,
+        worktree_store: Entity<WorktreeStore>,
+        limit: usize,
+    ) -> Self {
+        Self {
+            kind: SearchKind::OpenBuffersOnly,
+            buffer_store,
+            worktree_store,
+            limit,
+        }
+    }
+
+    pub(crate) const MAX_SEARCH_RESULT_FILES: usize = 5_000;
+    pub(crate) const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
+    /// Prepares a project search run. The resulting [`SearchResultsHandle`] has to be used to specify whether you're interested in matching buffers
+    /// or full search results.
+    pub fn into_handle(mut self, query: SearchQuery, cx: &mut App) -> SearchResultsHandle {
+        let mut open_buffers = HashSet::default();
+        let mut unnamed_buffers = Vec::new();
+        const MAX_CONCURRENT_BUFFER_OPENS: usize = 64;
+        let buffers = self.buffer_store.read(cx);
+        for handle in buffers.buffers() {
+            let buffer = handle.read(cx);
+            if !buffers.is_searchable(&buffer.remote_id()) {
+                continue;
+            } else if let Some(entry_id) = buffer.entry_id(cx) {
+                open_buffers.insert(entry_id);
+            } else {
+                self.limit -= self.limit.saturating_sub(1);
+                unnamed_buffers.push(handle)
+            };
+        }
+        let executor = cx.background_executor().clone();
+        let (tx, rx) = unbounded();
+        let (grab_buffer_snapshot_tx, grab_buffer_snapshot_rx) = unbounded();
+        let matching_buffers = grab_buffer_snapshot_rx.clone();
+        let trigger_search = Box::new(move |cx: &mut App| {
+            cx.spawn(async move |cx| {
+                for buffer in unnamed_buffers {
+                    _ = grab_buffer_snapshot_tx.send(buffer).await;
+                }
+
+                let (find_all_matches_tx, find_all_matches_rx) =
+                    bounded(MAX_CONCURRENT_BUFFER_OPENS);
+
+                let (candidate_searcher, tasks) = match self.kind {
+                    SearchKind::OpenBuffersOnly => {
+                        let Ok(open_buffers) = cx.update(|cx| self.all_loaded_buffers(&query, cx))
+                        else {
+                            return;
+                        };
+                        let fill_requests = cx
+                            .background_spawn(async move {
+                                for buffer in open_buffers {
+                                    if let Err(_) = grab_buffer_snapshot_tx.send(buffer).await {
+                                        return;
+                                    }
+                                }
+                            })
+                            .boxed_local();
+                        (FindSearchCandidates::OpenBuffersOnly, vec![fill_requests])
+                    }
+                    SearchKind::Local {
+                        fs,
+                        ref mut worktrees,
+                    } => {
+                        let (get_buffer_for_full_scan_tx, get_buffer_for_full_scan_rx) =
+                            unbounded();
+                        let (confirm_contents_will_match_tx, confirm_contents_will_match_rx) =
+                            bounded(64);
+                        let (sorted_search_results_tx, sorted_search_results_rx) = unbounded();
+
+                        let (input_paths_tx, input_paths_rx) = unbounded();
+
+                        let tasks = vec![
+                            cx.spawn(Self::provide_search_paths(
+                                std::mem::take(worktrees),
+                                query.include_ignored(),
+                                input_paths_tx,
+                                sorted_search_results_tx,
+                            ))
+                            .boxed_local(),
+                            Self::open_buffers(
+                                &self.buffer_store,
+                                get_buffer_for_full_scan_rx,
+                                grab_buffer_snapshot_tx,
+                                cx.clone(),
+                            )
+                            .boxed_local(),
+                            cx.background_spawn(Self::maintain_sorted_search_results(
+                                sorted_search_results_rx,
+                                get_buffer_for_full_scan_tx.clone(),
+                                self.limit,
+                            ))
+                            .boxed_local(),
+                        ];
+                        (
+                            FindSearchCandidates::Local {
+                                fs,
+                                get_buffer_for_full_scan_tx,
+                                confirm_contents_will_match_tx,
+                                confirm_contents_will_match_rx,
+                                input_paths_rx,
+                            },
+                            tasks,
+                        )
+                    }
+                    SearchKind::Remote {
+                        client,
+                        remote_id,
+                        models,
+                    } => {
+                        let request = client.request(proto::FindSearchCandidates {
+                            project_id: remote_id,
+                            query: Some(query.to_proto()),
+                            limit: self.limit as _,
+                        });
+                        let Ok(guard) = cx.update(|cx| {
+                            Project::retain_remotely_created_models_impl(
+                                &models,
+                                &self.buffer_store,
+                                &self.worktree_store,
+                                cx,
+                            )
+                        }) else {
+                            return;
+                        };
+                        let buffer_store = self.buffer_store.downgrade();
+                        let issue_remote_buffers_request = cx
+                            .spawn(async move |cx| {
+                                let _ = maybe!(async move {
+                                    let response = request.await?;
+
+                                    for buffer_id in response.buffer_ids {
+                                        let buffer_id = BufferId::new(buffer_id)?;
+                                        let buffer = buffer_store
+                                            .update(cx, |buffer_store, cx| {
+                                                buffer_store.wait_for_remote_buffer(buffer_id, cx)
+                                            })?
+                                            .await?;
+                                        let _ = grab_buffer_snapshot_tx.send(buffer).await;
+                                    }
+
+                                    drop(guard);
+                                    anyhow::Ok(())
+                                })
+                                .await
+                                .log_err();
+                            })
+                            .boxed_local();
+                        (
+                            FindSearchCandidates::Remote,
+                            vec![issue_remote_buffers_request],
+                        )
+                    }
+                };
+
+                let matches_count = AtomicUsize::new(0);
+                let matched_buffer_count = AtomicUsize::new(0);
+
+                let worker_pool = executor.scoped(|scope| {
+                    let num_cpus = executor.num_cpus();
+
+                    assert!(num_cpus > 0);
+                    for _ in 0..executor.num_cpus() - 1 {
+                        let worker = Worker {
+                            query: &query,
+                            open_buffers: &open_buffers,
+                            matched_buffer_count: &matched_buffer_count,
+                            matches_count: &matches_count,
+                            candidates: candidate_searcher.clone(),
+                            find_all_matches_rx: find_all_matches_rx.clone(),
+                            publish_matches: tx.clone(),
+                        };
+                        scope.spawn(worker.run());
+                    }
+                    drop(tx);
+                    drop(find_all_matches_rx);
+                    drop(candidate_searcher);
+                });
+
+                let buffer_snapshots = Self::grab_buffer_snapshots(
+                    grab_buffer_snapshot_rx,
+                    find_all_matches_tx,
+                    cx.clone(),
+                );
+                futures::future::join_all(
+                    [worker_pool.boxed_local(), buffer_snapshots.boxed_local()]
+                        .into_iter()
+                        .chain(tasks),
+                )
+                .await;
+            })
+        });
+
+        SearchResultsHandle {
+            results: rx,
+            matching_buffers,
+            trigger_search,
+        }
+    }
+
+    fn provide_search_paths(
+        worktrees: Vec<Entity<Worktree>>,
+        include_ignored: bool,
+        tx: Sender<InputPath>,
+        results: Sender<oneshot::Receiver<ProjectPath>>,
+    ) -> impl AsyncFnOnce(&mut AsyncApp) {
+        async move |cx| {
+            _ = maybe!(async move {
+                for worktree in worktrees {
+                    let (mut snapshot, worktree_settings) = worktree
+                        .read_with(cx, |this, _| {
+                            Some((this.snapshot(), this.as_local()?.settings()))
+                        })?
+                        .context("The worktree is not local")?;
+                    if include_ignored {
+                        // Pre-fetch all of the ignored directories as they're going to be searched.
+                        let mut entries_to_refresh = vec![];
+                        for entry in snapshot.entries(include_ignored, 0) {
+                            if entry.is_ignored && entry.kind.is_unloaded() {
+                                if !worktree_settings.is_path_excluded(&entry.path) {
+                                    entries_to_refresh.push(entry.path.clone());
+                                }
+                            }
+                        }
+                        let barrier = worktree.update(cx, |this, _| {
+                            let local = this.as_local_mut()?;
+                            let barrier = entries_to_refresh
+                                .into_iter()
+                                .map(|path| local.add_path_prefix_to_scan(path).into_future())
+                                .collect::<Vec<_>>();
+                            Some(barrier)
+                        })?;
+                        if let Some(barriers) = barrier {
+                            futures::future::join_all(barriers).await;
+                        }
+                        snapshot = worktree.read_with(cx, |this, _| this.snapshot())?;
+                    }
+                    cx.background_executor()
+                        .scoped(|scope| {
+                            scope.spawn(async {
+                                for entry in snapshot.files(include_ignored, 0) {
+                                    let (should_scan_tx, should_scan_rx) = oneshot::channel();
+                                    let Ok(_) = tx
+                                        .send(InputPath {
+                                            entry: entry.clone(),
+                                            snapshot: snapshot.clone(),
+                                            should_scan_tx,
+                                        })
+                                        .await
+                                    else {
+                                        return;
+                                    };
+                                    if results.send(should_scan_rx).await.is_err() {
+                                        return;
+                                    };
+                                }
+                            })
+                        })
+                        .await;
+                }
+                anyhow::Ok(())
+            })
+            .await;
+        }
+    }
+
+    async fn maintain_sorted_search_results(
+        rx: Receiver<oneshot::Receiver<ProjectPath>>,
+        paths_for_full_scan: Sender<ProjectPath>,
+        limit: usize,
+    ) {
+        let mut rx = pin!(rx);
+        let mut matched = 0;
+        while let Some(mut next_path_result) = rx.next().await {
+            let Some(successful_path) = next_path_result.next().await else {
+                // This math did not produce a match, hence skip it.
+                continue;
+            };
+            if paths_for_full_scan.send(successful_path).await.is_err() {
+                return;
+            };
+            matched += 1;
+            if matched >= limit {
+                break;
+            }
+        }
+    }
+
+    /// Background workers cannot open buffers by themselves, hence main thread will do it on their behalf.
+    async fn open_buffers(
+        buffer_store: &Entity<BufferStore>,
+        rx: Receiver<ProjectPath>,
+        find_all_matches_tx: Sender<Entity<Buffer>>,
+        mut cx: AsyncApp,
+    ) {
+        let mut rx = pin!(rx.ready_chunks(64));
+        _ = maybe!(async move {
+            while let Some(requested_paths) = rx.next().await {
+                let mut buffers = buffer_store.update(&mut cx, |this, cx| {
+                    requested_paths
+                        .into_iter()
+                        .map(|path| this.open_buffer(path, None, false, true, cx))
+                        .collect::<FuturesOrdered<_>>()
+                })?;
+
+                while let Some(buffer) = buffers.next().await {
+                    if let Some(buffer) = buffer.log_err() {
+                        find_all_matches_tx.send(buffer).await?;
+                    }
+                }
+            }
+            Result::<_, anyhow::Error>::Ok(())
+        })
+        .await;
+    }
+
+    async fn grab_buffer_snapshots(
+        rx: Receiver<Entity<Buffer>>,
+        find_all_matches_tx: Sender<(Entity<Buffer>, BufferSnapshot)>,
+        mut cx: AsyncApp,
+    ) {
+        _ = maybe!(async move {
+            while let Ok(buffer) = rx.recv().await {
+                let snapshot = buffer.read_with(&mut cx, |this, _| this.snapshot())?;
+                find_all_matches_tx.send((buffer, snapshot)).await?;
+            }
+            Result::<_, anyhow::Error>::Ok(())
+        })
+        .await;
+    }
+
+    fn all_loaded_buffers(&self, search_query: &SearchQuery, cx: &App) -> Vec<Entity<Buffer>> {
+        let worktree_store = self.worktree_store.read(cx);
+        let mut buffers = search_query
+            .buffers()
+            .into_iter()
+            .flatten()
+            .filter(|buffer| {
+                let b = buffer.read(cx);
+                if let Some(file) = b.file() {
+                    if !search_query.match_path(file.path().as_std_path()) {
+                        return false;
+                    }
+                    if !search_query.include_ignored()
+                        && let Some(entry) = b
+                            .entry_id(cx)
+                            .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx))
+                        && entry.is_ignored
+                    {
+                        return false;
+                    }
+                }
+                true
+            })
+            .cloned()
+            .collect::<Vec<_>>();
+        buffers.sort_by(|a, b| {
+            let a = a.read(cx);
+            let b = b.read(cx);
+            match (a.file(), b.file()) {
+                (None, None) => a.remote_id().cmp(&b.remote_id()),
+                (None, Some(_)) => std::cmp::Ordering::Less,
+                (Some(_), None) => std::cmp::Ordering::Greater,
+                (Some(a), Some(b)) => compare_rel_paths((a.path(), true), (b.path(), true)),
+            }
+        });
+
+        buffers
+    }
+}
+
+struct Worker<'search> {
+    query: &'search SearchQuery,
+    matched_buffer_count: &'search AtomicUsize,
+    matches_count: &'search AtomicUsize,
+    open_buffers: &'search HashSet<ProjectEntryId>,
+    candidates: FindSearchCandidates,
+    /// Ok, we're back in background: run full scan & find all matches in a given buffer snapshot.
+    find_all_matches_rx: Receiver<(Entity<Buffer>, BufferSnapshot)>,
+    /// Cool, we have results; let's share them with the world.
+    publish_matches: Sender<SearchResult>,
+}
+
+impl Worker<'_> {
+    async fn run(mut self) {
+        let (
+            input_paths_rx,
+            confirm_contents_will_match_rx,
+            mut confirm_contents_will_match_tx,
+            mut get_buffer_for_full_scan_tx,
+            fs,
+        ) = match self.candidates {
+            FindSearchCandidates::Local {
+                fs,
+                input_paths_rx,
+                confirm_contents_will_match_rx,
+                confirm_contents_will_match_tx,
+                get_buffer_for_full_scan_tx,
+            } => (
+                input_paths_rx,
+                confirm_contents_will_match_rx,
+                confirm_contents_will_match_tx,
+                get_buffer_for_full_scan_tx,
+                Some(fs),
+            ),
+            FindSearchCandidates::Remote | FindSearchCandidates::OpenBuffersOnly => (
+                unbounded().1,
+                unbounded().1,
+                unbounded().0,
+                unbounded().0,
+                None,
+            ),
+        };
+        let mut find_all_matches = pin!(self.find_all_matches_rx.fuse());
+        let mut find_first_match = pin!(confirm_contents_will_match_rx.fuse());
+        let mut scan_path = pin!(input_paths_rx.fuse());
+
+        loop {
+            let handler = RequestHandler {
+                query: self.query,
+                open_entries: &self.open_buffers,
+                fs: fs.as_deref(),
+                matched_buffer_count: self.matched_buffer_count,
+                matches_count: self.matches_count,
+                confirm_contents_will_match_tx: &confirm_contents_will_match_tx,
+                get_buffer_for_full_scan_tx: &get_buffer_for_full_scan_tx,
+                publish_matches: &self.publish_matches,
+            };
+            // Whenever we notice that some step of a pipeline is closed, we don't want to close subsequent
+            // steps straight away. Another worker might be about to produce a value that will
+            // be pushed there, thus we'll replace current worker's pipe with a dummy one.
+            // That way, we'll only ever close a next-stage channel when ALL workers do so.
+            select_biased! {
+                find_all_matches = find_all_matches.next() => {
+
+                    if self.publish_matches.is_closed() {
+                        break;
+                    }
+                    let Some(matches) = find_all_matches else {
+                        self.publish_matches = bounded(1).0;
+                        continue;
+                    };
+                    let result = handler.handle_find_all_matches(matches).await;
+                    if let Some(_should_bail) = result {
+
+                        self.publish_matches = bounded(1).0;
+                        continue;
+                    }
+                },
+                find_first_match = find_first_match.next() => {
+                    if let Some(buffer_with_at_least_one_match) = find_first_match {
+                        handler.handle_find_first_match(buffer_with_at_least_one_match).await;
+                    } else {
+                        get_buffer_for_full_scan_tx = bounded(1).0;
+                    }
+
+                },
+                scan_path = scan_path.next() => {
+                    if let Some(path_to_scan) = scan_path {
+                        handler.handle_scan_path(path_to_scan).await;
+                    } else {
+                        // If we're the last worker to notice that this is not producing values, close the upstream.
+                        confirm_contents_will_match_tx = bounded(1).0;
+                    }
+
+                 }
+                 complete => {
+                     break
+                },
+
+            }
+        }
+    }
+}
+
+struct RequestHandler<'worker> {
+    query: &'worker SearchQuery,
+    fs: Option<&'worker dyn Fs>,
+    open_entries: &'worker HashSet<ProjectEntryId>,
+    matched_buffer_count: &'worker AtomicUsize,
+    matches_count: &'worker AtomicUsize,
+
+    confirm_contents_will_match_tx: &'worker Sender<MatchingEntry>,
+    get_buffer_for_full_scan_tx: &'worker Sender<ProjectPath>,
+    publish_matches: &'worker Sender<SearchResult>,
+}
+
+struct LimitReached;
+
+impl RequestHandler<'_> {
+    async fn handle_find_all_matches(
+        &self,
+        (buffer, snapshot): (Entity<Buffer>, BufferSnapshot),
+    ) -> Option<LimitReached> {
+        let ranges = self
+            .query
+            .search(&snapshot, None)
+            .await
+            .iter()
+            .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
+            .collect::<Vec<_>>();
+
+        let matched_ranges = ranges.len();
+        if self.matched_buffer_count.fetch_add(1, Ordering::Release)
+            > Search::MAX_SEARCH_RESULT_FILES
+            || self
+                .matches_count
+                .fetch_add(matched_ranges, Ordering::Release)
+                > Search::MAX_SEARCH_RESULT_RANGES
+        {
+            _ = self.publish_matches.send(SearchResult::LimitReached).await;
+            Some(LimitReached)
+        } else {
+            _ = self
+                .publish_matches
+                .send(SearchResult::Buffer { buffer, ranges })
+                .await;
+            None
+        }
+    }
+    async fn handle_find_first_match(&self, mut entry: MatchingEntry) {
+        _=maybe!(async move {
+            let abs_path = entry.worktree_root.join(entry.path.path.as_std_path());
+            let Some(file) = self.fs.context("Trying to query filesystem in remote project search")?.open_sync(&abs_path).await.log_err() else {
+                return anyhow::Ok(());
+            };
+
+            let mut file = BufReader::new(file);
+            let file_start = file.fill_buf()?;
+
+            if let Err(Some(starting_position)) =
+            std::str::from_utf8(file_start).map_err(|e| e.error_len())
+            {
+                // Before attempting to match the file content, throw away files that have invalid UTF-8 sequences early on;
+                // That way we can still match files in a streaming fashion without having look at "obviously binary" files.
+                log::debug!(
+                    "Invalid UTF-8 sequence in file {abs_path:?} at byte position {starting_position}"
+                );
+                return Ok(());
+            }
+
+            if self.query.detect(file).unwrap_or(false) {
+                // Yes, we should scan the whole file.
+                entry.should_scan_tx.send(entry.path).await?;
+            }
+            Ok(())
+        }).await;
+    }
+
+    async fn handle_scan_path(&self, req: InputPath) {
+        _ = maybe!(async move {
+            let InputPath {
+                entry,
+
+                snapshot,
+                should_scan_tx,
+            } = req;
+
+            if entry.is_fifo || !entry.is_file() {
+                return Ok(());
+            }
+
+            if self.query.filters_path() {
+                let matched_path = if self.query.match_full_paths() {
+                    let mut full_path = snapshot.root_name().as_std_path().to_owned();
+                    full_path.push(entry.path.as_std_path());
+                    self.query.match_path(&full_path)
+                } else {
+                    self.query.match_path(entry.path.as_std_path())
+                };
+                if !matched_path {
+                    return Ok(());
+                }
+            }
+
+            if self.open_entries.contains(&entry.id) {
+                // The buffer is already in memory and that's the version we want to scan;
+                // hence skip the dilly-dally and look for all matches straight away.
+                self.get_buffer_for_full_scan_tx
+                    .send(ProjectPath {
+                        worktree_id: snapshot.id(),
+                        path: entry.path.clone(),
+                    })
+                    .await?;
+            } else {
+                self.confirm_contents_will_match_tx
+                    .send(MatchingEntry {
+                        should_scan_tx: should_scan_tx,
+                        worktree_root: snapshot.abs_path().clone(),
+                        path: ProjectPath {
+                            worktree_id: snapshot.id(),
+                            path: entry.path.clone(),
+                        },
+                    })
+                    .await?;
+            }
+
+            anyhow::Ok(())
+        })
+        .await;
+    }
+}
+
+struct InputPath {
+    entry: Entry,
+    snapshot: Snapshot,
+    should_scan_tx: oneshot::Sender<ProjectPath>,
+}
+
+struct MatchingEntry {
+    worktree_root: Arc<Path>,
+    path: ProjectPath,
+    should_scan_tx: oneshot::Sender<ProjectPath>,
+}

crates/project/src/project_tests.rs 🔗

@@ -3878,7 +3878,14 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         worktree
             .update(cx, |worktree, cx| {
-                worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), cx)
+                worktree.load_file(
+                    rel_path("dir1/dir2/dir3/test.txt"),
+                    None,
+                    false,
+                    true,
+                    None,
+                    cx,
+                )
             })
             .await
             .unwrap()
@@ -3925,7 +3932,7 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         worktree
             .update(cx, |worktree, cx| {
-                worktree.load_file(rel_path("dir1/dir2/test.txt"), cx)
+                worktree.load_file(rel_path("dir1/dir2/test.txt"), None, false, true, None, cx)
             })
             .await
             .unwrap()
@@ -9003,7 +9010,14 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
     let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
     tree.flush_fs_events(cx).await;
     tree.update(cx, |tree, cx| {
-        tree.load_file(rel_path("project/target/debug/important_text.txt"), cx)
+        tree.load_file(
+            rel_path("project/target/debug/important_text.txt"),
+            None,
+            false,
+            true,
+            None,
+            cx,
+        )
     })
     .await
     .unwrap();
@@ -9164,7 +9178,14 @@ async fn test_odd_events_for_ignored_dirs(
 
     let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
     tree.update(cx, |tree, cx| {
-        tree.load_file(rel_path("target/debug/foo.txt"), cx)
+        tree.load_file(
+            rel_path("target/debug/foo.txt"),
+            None,
+            false,
+            true,
+            None,
+            cx,
+        )
     })
     .await
     .unwrap();

crates/remote_server/src/headless_project.rs 🔗

@@ -506,16 +506,7 @@ impl HeadlessProject {
         let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
             let buffer_store = this.buffer_store.clone();
             let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
-                buffer_store.open_buffer(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::<Path>::from_proto(message.payload.path),
-                    },
-                    None,
-                    false,
-                    true,
-                    cx,
-                )
+                buffer_store.open_buffer(ProjectPath { worktree_id, path }, None, false, true, cx)
             });
             anyhow::Ok((buffer_store, buffer))
         })??;

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -6,7 +6,6 @@ use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream};
 use client::{Client, UserStore};
 use clock::FakeSystemClock;
 use collections::{HashMap, HashSet};
-use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel};
 
 use encodings::Encoding;
 use extension::ExtensionHostProxy;
@@ -17,6 +16,7 @@ use language::{
     Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Rope,
     language_settings::{AllLanguageSettings, language_settings},
 };
+use language_model::LanguageModelToolResultContent;
 use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
 use node_runtime::NodeRuntime;
 use project::{

crates/worktree/src/worktree.rs 🔗

@@ -706,7 +706,7 @@ impl Worktree {
 
     pub fn load_file(
         &self,
-        path: &Path,
+        path: &RelPath,
         encoding: Option<Encoding>,
         force: bool,
         detect_utf16: bool,
@@ -1326,7 +1326,7 @@ impl LocalWorktree {
 
     fn load_file(
         &self,
-        path: &Path,
+        path: &RelPath,
         encoding: Option<Encoding>,
         force: bool,
         detect_utf16: bool,
@@ -3189,7 +3189,7 @@ impl language::LocalFile for File {
         let abs_path = worktree.absolutize(&self.path);
         let fs = worktree.fs.clone();
         cx.background_spawn(async move {
-            fs.load_with_encoding(&abs_path?, encoding, force, detect_utf16, buffer_encoding)
+            fs.load_with_encoding(&abs_path, encoding, force, detect_utf16, buffer_encoding)
                 .await
         })
     }

crates/worktree/src/worktree_tests.rs 🔗

@@ -468,7 +468,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
     let loaded = tree
         .update(cx, |tree, cx| {
             tree.load_file(
-                "one/node_modules/b/b1.js".as_ref(),
+                rel_path("one/node_modules/b/b1.js"),
                 None,
                 false,
                 false,
@@ -515,7 +515,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
     let loaded = tree
         .update(cx, |tree, cx| {
             tree.load_file(
-                "one/node_modules/a/a2.js".as_ref(),
+                rel_path("one/node_modules/a/a2.js"),
                 None,
                 false,
                 false,
@@ -1966,101 +1966,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
         .collect()
 }
 
-#[gpui::test]
-async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
-    init_test(cx);
-    let fs = FakeFs::new(cx.background_executor.clone());
-    let expected_contents = "content";
-    fs.as_fake()
-        .insert_tree(
-            "/root",
-            json!({
-                "test.txt": expected_contents
-            }),
-        )
-        .await;
-    let worktree = Worktree::local(
-        Path::new("/root"),
-        true,
-        fs.clone(),
-        Arc::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    let entry_id = worktree.read_with(cx, |worktree, _| {
-        worktree.entry_for_path("test.txt").unwrap().id
-    });
-    let _result = worktree
-        .update(cx, |worktree, cx| {
-            worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
-        })
-        .await
-        .unwrap();
-    worktree.read_with(cx, |worktree, _| {
-        assert!(
-            worktree.entry_for_path("test.txt").is_none(),
-            "Old file should have been removed"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
-            "Whole directory hierarchy and the new file should have been created"
-        );
-    });
-    assert_eq!(
-        worktree
-            .update(cx, |worktree, cx| {
-                worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), None, cx)
-            })
-            .await
-            .unwrap()
-            .text,
-        expected_contents,
-        "Moved file's contents should be preserved"
-    );
-
-    let entry_id = worktree.read_with(cx, |worktree, _| {
-        worktree
-            .entry_for_path("dir1/dir2/dir3/test.txt")
-            .unwrap()
-            .id
-    });
-    let _result = worktree
-        .update(cx, |worktree, cx| {
-            worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
-        })
-        .await
-        .unwrap();
-    worktree.read_with(cx, |worktree, _| {
-        assert!(
-            worktree.entry_for_path("test.txt").is_none(),
-            "First file should not reappear"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
-            "Old file should have been removed"
-        );
-        assert!(
-            worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
-            "No error should have occurred after moving into existing directory"
-        );
-    });
-    assert_eq!(
-        worktree
-            .update(cx, |worktree, cx| {
-                worktree.load_file("dir1/dir2/test.txt".as_ref(), None, cx)
-            })
-            .await
-            .unwrap()
-            .text,
-        expected_contents,
-        "Moved file's contents should be preserved"
-    );
-}
-
 #[gpui::test]
 async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
     init_test(cx);

crates/zed/Cargo.toml 🔗

@@ -52,7 +52,6 @@ debugger_ui.workspace = true
 diagnostics.workspace = true
 editor.workspace = true
 zeta2_tools.workspace = true
-edit_prediction_tools.workspace = true
 encodings.workspace = true
 encodings_ui.workspace = true
 env_logger.workspace = true

crates/zed/src/zed/open_listener.rs 🔗

@@ -653,6 +653,7 @@ mod tests {
         ipc::{self},
     };
     use editor::Editor;
+    use encodings::Encoding;
     use gpui::TestAppContext;
     use language::LineEnding;
     use remote::SshConnectionOptions;
@@ -863,6 +864,7 @@ mod tests {
                 Path::new(file1_path),
                 &Rope::from_str("content1", cx.background_executor()),
                 LineEnding::Unix,
+                Encoding::default(),
             )
             .await
             .unwrap();
@@ -877,6 +879,7 @@ mod tests {
                 Path::new(file2_path),
                 &Rope::from_str("content2", cx.background_executor()),
                 LineEnding::Unix,
+                Encoding::default(),
             )
             .await
             .unwrap();

crates/zeta/src/zeta.rs 🔗

@@ -1986,7 +1986,7 @@ mod tests {
                     .worktree_for_root_name("closed_source_worktree", cx)
                     .unwrap();
                 worktree2.update(cx, |worktree2, cx| {
-                    worktree2.load_file(Path::new("main.rs"), None, false, true, None, cx)
+                    worktree2.load_file(rel_path("main.rs"), None, false, true, None, cx)
                 })
             })
             .await