edit_file_tool.rs

   1use crate::{AgentTool, Thread, ToolCallEventStream};
   2use acp_thread::Diff;
   3use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
   4use anyhow::{Context as _, Result, anyhow};
   5use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
   6use cloud_llm_client::CompletionIntent;
   7use collections::HashSet;
   8use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
   9use indoc::formatdoc;
  10use language::language_settings::{self, FormatOnSave};
  11use language::{LanguageRegistry, ToPoint};
  12use language_model::LanguageModelToolResultContent;
  13use paths;
  14use project::lsp_store::{FormatTrigger, LspFormatTarget};
  15use project::{Project, ProjectPath};
  16use schemars::JsonSchema;
  17use serde::{Deserialize, Serialize};
  18use settings::Settings;
  19use smol::stream::StreamExt as _;
  20use std::path::{Path, PathBuf};
  21use std::sync::Arc;
  22use ui::SharedString;
  23use util::ResultExt;
  24
  25const DEFAULT_UI_TEXT: &str = "Editing file";
  26
  27/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
  28///
  29/// Before using this tool:
  30///
  31/// 1. Use the `read_file` tool to understand the file's contents and context
  32///
  33/// 2. Verify the directory path is correct (only applicable when creating new files):
  34///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location
  35#[derive(Debug, Serialize, Deserialize, JsonSchema)]
  36pub struct EditFileToolInput {
  37    /// 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.
  38    ///
  39    /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
  40    ///
  41    /// NEVER mention the file path in this description.
  42    ///
  43    /// <example>Fix API endpoint URLs</example>
  44    /// <example>Update copyright year in `page_footer`</example>
  45    ///
  46    /// Make sure to include this field before all the others in the input object so that we can display it immediately.
  47    pub display_description: String,
  48
  49    /// The full path of the file to create or modify in the project.
  50    ///
  51    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
  52    ///
  53    /// The following examples assume we have two root directories in the project:
  54    /// - /a/b/backend
  55    /// - /c/d/frontend
  56    ///
  57    /// <example>
  58    /// `backend/src/main.rs`
  59    ///
  60    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
  61    /// </example>
  62    ///
  63    /// <example>
  64    /// `frontend/db.js`
  65    /// </example>
  66    pub path: PathBuf,
  67    /// The mode of operation on the file. Possible values:
  68    /// - 'edit': Make granular edits to an existing file.
  69    /// - 'create': Create a new file if it doesn't exist.
  70    /// - 'overwrite': Replace the entire contents of an existing file.
  71    ///
  72    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
  73    pub mode: EditFileMode,
  74}
  75
  76#[derive(Debug, Serialize, Deserialize, JsonSchema)]
  77struct EditFileToolPartialInput {
  78    #[serde(default)]
  79    path: String,
  80    #[serde(default)]
  81    display_description: String,
  82}
  83
  84#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  85#[serde(rename_all = "lowercase")]
  86pub enum EditFileMode {
  87    Edit,
  88    Create,
  89    Overwrite,
  90}
  91
  92#[derive(Debug, Serialize, Deserialize)]
  93pub struct EditFileToolOutput {
  94    #[serde(alias = "original_path")]
  95    input_path: PathBuf,
  96    new_text: String,
  97    old_text: Arc<String>,
  98    #[serde(default)]
  99    diff: String,
 100    #[serde(alias = "raw_output")]
 101    edit_agent_output: EditAgentOutput,
 102}
 103
 104impl From<EditFileToolOutput> for LanguageModelToolResultContent {
 105    fn from(output: EditFileToolOutput) -> Self {
 106        if output.diff.is_empty() {
 107            "No edits were made.".into()
 108        } else {
 109            format!(
 110                "Edited {}:\n\n```diff\n{}\n```",
 111                output.input_path.display(),
 112                output.diff
 113            )
 114            .into()
 115        }
 116    }
 117}
 118
 119pub struct EditFileTool {
 120    thread: WeakEntity<Thread>,
 121    language_registry: Arc<LanguageRegistry>,
 122}
 123
 124impl EditFileTool {
 125    pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
 126        Self {
 127            thread,
 128            language_registry,
 129        }
 130    }
 131
 132    fn authorize(
 133        &self,
 134        input: &EditFileToolInput,
 135        event_stream: &ToolCallEventStream,
 136        cx: &mut App,
 137    ) -> Task<Result<()>> {
 138        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
 139            return Task::ready(Ok(()));
 140        }
 141
 142        // If any path component matches the local settings folder, then this could affect
 143        // the editor in ways beyond the project source, so prompt.
 144        let local_settings_folder = paths::local_settings_folder_relative_path();
 145        let path = Path::new(&input.path);
 146        if path
 147            .components()
 148            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
 149        {
 150            return event_stream.authorize(
 151                format!("{} (local settings)", input.display_description),
 152                cx,
 153            );
 154        }
 155
 156        // It's also possible that the global config dir is configured to be inside the project,
 157        // so check for that edge case too.
 158        if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
 159            && canonical_path.starts_with(paths::config_dir())
 160        {
 161            return event_stream.authorize(
 162                format!("{} (global settings)", input.display_description),
 163                cx,
 164            );
 165        }
 166
 167        // Check if path is inside the global config directory
 168        // First check if it's already inside project - if not, try to canonicalize
 169        let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
 170            thread.project().read(cx).find_project_path(&input.path, cx)
 171        }) else {
 172            return Task::ready(Err(anyhow!("thread was dropped")));
 173        };
 174
 175        // If the path is inside the project, and it's not one of the above edge cases,
 176        // then no confirmation is necessary. Otherwise, confirmation is necessary.
 177        if project_path.is_some() {
 178            Task::ready(Ok(()))
 179        } else {
 180            event_stream.authorize(&input.display_description, cx)
 181        }
 182    }
 183}
 184
 185impl AgentTool for EditFileTool {
 186    type Input = EditFileToolInput;
 187    type Output = EditFileToolOutput;
 188
 189    fn name(&self) -> SharedString {
 190        "edit_file".into()
 191    }
 192
 193    fn kind(&self) -> acp::ToolKind {
 194        acp::ToolKind::Edit
 195    }
 196
 197    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
 198        match input {
 199            Ok(input) => input.display_description.into(),
 200            Err(raw_input) => {
 201                if let Some(input) =
 202                    serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
 203                {
 204                    let description = input.display_description.trim();
 205                    if !description.is_empty() {
 206                        return description.to_string().into();
 207                    }
 208
 209                    let path = input.path.trim().to_string();
 210                    if !path.is_empty() {
 211                        return path.into();
 212                    }
 213                }
 214
 215                DEFAULT_UI_TEXT.into()
 216            }
 217        }
 218    }
 219
 220    fn run(
 221        self: Arc<Self>,
 222        input: Self::Input,
 223        event_stream: ToolCallEventStream,
 224        cx: &mut App,
 225    ) -> Task<Result<Self::Output>> {
 226        let Ok(project) = self
 227            .thread
 228            .read_with(cx, |thread, _cx| thread.project().clone())
 229        else {
 230            return Task::ready(Err(anyhow!("thread was dropped")));
 231        };
 232        let project_path = match resolve_path(&input, project.clone(), cx) {
 233            Ok(path) => path,
 234            Err(err) => return Task::ready(Err(anyhow!(err))),
 235        };
 236        let abs_path = project.read(cx).absolute_path(&project_path, cx);
 237        if let Some(abs_path) = abs_path.clone() {
 238            event_stream.update_fields(ToolCallUpdateFields {
 239                locations: Some(vec![acp::ToolCallLocation {
 240                    path: abs_path,
 241                    line: None,
 242                }]),
 243                ..Default::default()
 244            });
 245        }
 246
 247        let authorize = self.authorize(&input, &event_stream, cx);
 248        cx.spawn(async move |cx: &mut AsyncApp| {
 249            authorize.await?;
 250
 251            let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
 252                let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
 253                (request, thread.model().cloned(), thread.action_log().clone())
 254            })?;
 255            let request = request?;
 256            let model = model.context("No language model configured")?;
 257
 258            let edit_format = EditFormat::from_model(model.clone())?;
 259            let edit_agent = EditAgent::new(
 260                model,
 261                project.clone(),
 262                action_log.clone(),
 263                // TODO: move edit agent to this crate so we can use our templates
 264                assistant_tools::templates::Templates::new(),
 265                edit_format,
 266            );
 267
 268            let buffer = project
 269                .update(cx, |project, cx| {
 270                    project.open_buffer(project_path.clone(), cx)
 271                })?
 272                .await?;
 273
 274            let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
 275            event_stream.update_diff(diff.clone());
 276
 277            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 278            let old_text = cx
 279                .background_spawn({
 280                    let old_snapshot = old_snapshot.clone();
 281                    async move { Arc::new(old_snapshot.text()) }
 282                })
 283                .await;
 284
 285
 286            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
 287                edit_agent.edit(
 288                    buffer.clone(),
 289                    input.display_description.clone(),
 290                    &request,
 291                    cx,
 292                )
 293            } else {
 294                edit_agent.overwrite(
 295                    buffer.clone(),
 296                    input.display_description.clone(),
 297                    &request,
 298                    cx,
 299                )
 300            };
 301
 302            let mut hallucinated_old_text = false;
 303            let mut ambiguous_ranges = Vec::new();
 304            let mut emitted_location = false;
 305            while let Some(event) = events.next().await {
 306                match event {
 307                    EditAgentOutputEvent::Edited(range) => {
 308                        if !emitted_location {
 309                            let line = buffer.update(cx, |buffer, _cx| {
 310                                range.start.to_point(&buffer.snapshot()).row
 311                            }).ok();
 312                            if let Some(abs_path) = abs_path.clone() {
 313                                event_stream.update_fields(ToolCallUpdateFields {
 314                                    locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
 315                                    ..Default::default()
 316                                });
 317                            }
 318                            emitted_location = true;
 319                        }
 320                    },
 321                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
 322                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
 323                    EditAgentOutputEvent::ResolvingEditRange(range) => {
 324                        diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
 325                        // if !emitted_location {
 326                        //     let line = buffer.update(cx, |buffer, _cx| {
 327                        //         range.start.to_point(&buffer.snapshot()).row
 328                        //     }).ok();
 329                        //     if let Some(abs_path) = abs_path.clone() {
 330                        //         event_stream.update_fields(ToolCallUpdateFields {
 331                        //             locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
 332                        //             ..Default::default()
 333                        //         });
 334                        //     }
 335                        // }
 336                    }
 337                }
 338            }
 339
 340            // If format_on_save is enabled, format the buffer
 341            let format_on_save_enabled = buffer
 342                .read_with(cx, |buffer, cx| {
 343                    let settings = language_settings::language_settings(
 344                        buffer.language().map(|l| l.name()),
 345                        buffer.file(),
 346                        cx,
 347                    );
 348                    settings.format_on_save != FormatOnSave::Off
 349                })
 350                .unwrap_or(false);
 351
 352            let edit_agent_output = output.await?;
 353
 354            if format_on_save_enabled {
 355                action_log.update(cx, |log, cx| {
 356                    log.buffer_edited(buffer.clone(), cx);
 357                })?;
 358
 359                let format_task = project.update(cx, |project, cx| {
 360                    project.format(
 361                        HashSet::from_iter([buffer.clone()]),
 362                        LspFormatTarget::Buffers,
 363                        false, // Don't push to history since the tool did it.
 364                        FormatTrigger::Save,
 365                        cx,
 366                    )
 367                })?;
 368                format_task.await.log_err();
 369            }
 370
 371            project
 372                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
 373                .await?;
 374
 375            action_log.update(cx, |log, cx| {
 376                log.buffer_edited(buffer.clone(), cx);
 377            })?;
 378
 379            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 380            let (new_text, unified_diff) = cx
 381                .background_spawn({
 382                    let new_snapshot = new_snapshot.clone();
 383                    let old_text = old_text.clone();
 384                    async move {
 385                        let new_text = new_snapshot.text();
 386                        let diff = language::unified_diff(&old_text, &new_text);
 387                        (new_text, diff)
 388                    }
 389                })
 390                .await;
 391
 392            diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
 393
 394            let input_path = input.path.display();
 395            if unified_diff.is_empty() {
 396                anyhow::ensure!(
 397                    !hallucinated_old_text,
 398                    formatdoc! {"
 399                        Some edits were produced but none of them could be applied.
 400                        Read the relevant sections of {input_path} again so that
 401                        I can perform the requested edits.
 402                    "}
 403                );
 404                anyhow::ensure!(
 405                    ambiguous_ranges.is_empty(),
 406                    {
 407                        let line_numbers = ambiguous_ranges
 408                            .iter()
 409                            .map(|range| range.start.to_string())
 410                            .collect::<Vec<_>>()
 411                            .join(", ");
 412                        formatdoc! {"
 413                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
 414                            relevant sections of {input_path} again and extend <old_text> so
 415                            that I can perform the requested edits.
 416                        "}
 417                    }
 418                );
 419            }
 420
 421            Ok(EditFileToolOutput {
 422                input_path: input.path,
 423                new_text,
 424                old_text,
 425                diff: unified_diff,
 426                edit_agent_output,
 427            })
 428        })
 429    }
 430
 431    fn replay(
 432        &self,
 433        _input: Self::Input,
 434        output: Self::Output,
 435        event_stream: ToolCallEventStream,
 436        cx: &mut App,
 437    ) -> Result<()> {
 438        event_stream.update_diff(cx.new(|cx| {
 439            Diff::finalized(
 440                output.input_path,
 441                Some(output.old_text.to_string()),
 442                output.new_text,
 443                self.language_registry.clone(),
 444                cx,
 445            )
 446        }));
 447        Ok(())
 448    }
 449}
 450
 451/// Validate that the file path is valid, meaning:
 452///
 453/// - For `edit` and `overwrite`, the path must point to an existing file.
 454/// - For `create`, the file must not already exist, but it's parent dir must exist.
 455fn resolve_path(
 456    input: &EditFileToolInput,
 457    project: Entity<Project>,
 458    cx: &mut App,
 459) -> Result<ProjectPath> {
 460    let project = project.read(cx);
 461
 462    match input.mode {
 463        EditFileMode::Edit | EditFileMode::Overwrite => {
 464            let path = project
 465                .find_project_path(&input.path, cx)
 466                .context("Can't edit file: path not found")?;
 467
 468            let entry = project
 469                .entry_for_path(&path, cx)
 470                .context("Can't edit file: path not found")?;
 471
 472            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
 473            Ok(path)
 474        }
 475
 476        EditFileMode::Create => {
 477            if let Some(path) = project.find_project_path(&input.path, cx) {
 478                anyhow::ensure!(
 479                    project.entry_for_path(&path, cx).is_none(),
 480                    "Can't create file: file already exists"
 481                );
 482            }
 483
 484            let parent_path = input
 485                .path
 486                .parent()
 487                .context("Can't create file: incorrect path")?;
 488
 489            let parent_project_path = project.find_project_path(&parent_path, cx);
 490
 491            let parent_entry = parent_project_path
 492                .as_ref()
 493                .and_then(|path| project.entry_for_path(path, cx))
 494                .context("Can't create file: parent directory doesn't exist")?;
 495
 496            anyhow::ensure!(
 497                parent_entry.is_dir(),
 498                "Can't create file: parent is not a directory"
 499            );
 500
 501            let file_name = input
 502                .path
 503                .file_name()
 504                .context("Can't create file: invalid filename")?;
 505
 506            let new_file_path = parent_project_path.map(|parent| ProjectPath {
 507                path: Arc::from(parent.path.join(file_name)),
 508                ..parent
 509            });
 510
 511            new_file_path.context("Can't create file")
 512        }
 513    }
 514}
 515
 516#[cfg(test)]
 517mod tests {
 518    use super::*;
 519    use crate::{ContextServerRegistry, Templates};
 520    use action_log::ActionLog;
 521    use client::TelemetrySettings;
 522    use fs::Fs;
 523    use gpui::{TestAppContext, UpdateGlobal};
 524    use language_model::fake_provider::FakeLanguageModel;
 525    use prompt_store::ProjectContext;
 526    use serde_json::json;
 527    use settings::SettingsStore;
 528    use util::path;
 529
 530    #[gpui::test]
 531    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
 532        init_test(cx);
 533
 534        let fs = project::FakeFs::new(cx.executor());
 535        fs.insert_tree("/root", json!({})).await;
 536        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 537        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
 538        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 539        let context_server_registry =
 540            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 541        let model = Arc::new(FakeLanguageModel::default());
 542        let thread = cx.new(|cx| {
 543            Thread::new(
 544                project,
 545                cx.new(|_cx| ProjectContext::default()),
 546                context_server_registry,
 547                action_log,
 548                Templates::new(),
 549                Some(model),
 550                cx,
 551            )
 552        });
 553        let result = cx
 554            .update(|cx| {
 555                let input = EditFileToolInput {
 556                    display_description: "Some edit".into(),
 557                    path: "root/nonexistent_file.txt".into(),
 558                    mode: EditFileMode::Edit,
 559                };
 560                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 561                    input,
 562                    ToolCallEventStream::test().0,
 563                    cx,
 564                )
 565            })
 566            .await;
 567        assert_eq!(
 568            result.unwrap_err().to_string(),
 569            "Can't edit file: path not found"
 570        );
 571    }
 572
 573    #[gpui::test]
 574    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
 575        let mode = &EditFileMode::Create;
 576
 577        let result = test_resolve_path(mode, "root/new.txt", cx);
 578        assert_resolved_path_eq(result.await, "new.txt");
 579
 580        let result = test_resolve_path(mode, "new.txt", cx);
 581        assert_resolved_path_eq(result.await, "new.txt");
 582
 583        let result = test_resolve_path(mode, "dir/new.txt", cx);
 584        assert_resolved_path_eq(result.await, "dir/new.txt");
 585
 586        let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
 587        assert_eq!(
 588            result.await.unwrap_err().to_string(),
 589            "Can't create file: file already exists"
 590        );
 591
 592        let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
 593        assert_eq!(
 594            result.await.unwrap_err().to_string(),
 595            "Can't create file: parent directory doesn't exist"
 596        );
 597    }
 598
 599    #[gpui::test]
 600    async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
 601        let mode = &EditFileMode::Edit;
 602
 603        let path_with_root = "root/dir/subdir/existing.txt";
 604        let path_without_root = "dir/subdir/existing.txt";
 605        let result = test_resolve_path(mode, path_with_root, cx);
 606        assert_resolved_path_eq(result.await, path_without_root);
 607
 608        let result = test_resolve_path(mode, path_without_root, cx);
 609        assert_resolved_path_eq(result.await, path_without_root);
 610
 611        let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
 612        assert_eq!(
 613            result.await.unwrap_err().to_string(),
 614            "Can't edit file: path not found"
 615        );
 616
 617        let result = test_resolve_path(mode, "root/dir", cx);
 618        assert_eq!(
 619            result.await.unwrap_err().to_string(),
 620            "Can't edit file: path is a directory"
 621        );
 622    }
 623
 624    async fn test_resolve_path(
 625        mode: &EditFileMode,
 626        path: &str,
 627        cx: &mut TestAppContext,
 628    ) -> anyhow::Result<ProjectPath> {
 629        init_test(cx);
 630
 631        let fs = project::FakeFs::new(cx.executor());
 632        fs.insert_tree(
 633            "/root",
 634            json!({
 635                "dir": {
 636                    "subdir": {
 637                        "existing.txt": "hello"
 638                    }
 639                }
 640            }),
 641        )
 642        .await;
 643        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 644
 645        let input = EditFileToolInput {
 646            display_description: "Some edit".into(),
 647            path: path.into(),
 648            mode: mode.clone(),
 649        };
 650
 651        cx.update(|cx| resolve_path(&input, project, cx))
 652    }
 653
 654    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
 655        let actual = path
 656            .expect("Should return valid path")
 657            .path
 658            .to_str()
 659            .unwrap()
 660            .replace("\\", "/"); // Naive Windows paths normalization
 661        assert_eq!(actual, expected);
 662    }
 663
 664    #[gpui::test]
 665    async fn test_format_on_save(cx: &mut TestAppContext) {
 666        init_test(cx);
 667
 668        let fs = project::FakeFs::new(cx.executor());
 669        fs.insert_tree("/root", json!({"src": {}})).await;
 670
 671        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 672
 673        // Set up a Rust language with LSP formatting support
 674        let rust_language = Arc::new(language::Language::new(
 675            language::LanguageConfig {
 676                name: "Rust".into(),
 677                matcher: language::LanguageMatcher {
 678                    path_suffixes: vec!["rs".to_string()],
 679                    ..Default::default()
 680                },
 681                ..Default::default()
 682            },
 683            None,
 684        ));
 685
 686        // Register the language and fake LSP
 687        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 688        language_registry.add(rust_language);
 689
 690        let mut fake_language_servers = language_registry.register_fake_lsp(
 691            "Rust",
 692            language::FakeLspAdapter {
 693                capabilities: lsp::ServerCapabilities {
 694                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
 695                    ..Default::default()
 696                },
 697                ..Default::default()
 698            },
 699        );
 700
 701        // Create the file
 702        fs.save(
 703            path!("/root/src/main.rs").as_ref(),
 704            &"initial content".into(),
 705            language::LineEnding::Unix,
 706        )
 707        .await
 708        .unwrap();
 709
 710        // Open the buffer to trigger LSP initialization
 711        let buffer = project
 712            .update(cx, |project, cx| {
 713                project.open_local_buffer(path!("/root/src/main.rs"), cx)
 714            })
 715            .await
 716            .unwrap();
 717
 718        // Register the buffer with language servers
 719        let _handle = project.update(cx, |project, cx| {
 720            project.register_buffer_with_language_servers(&buffer, cx)
 721        });
 722
 723        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
 724        const FORMATTED_CONTENT: &str =
 725            "This file was formatted by the fake formatter in the test.\n";
 726
 727        // Get the fake language server and set up formatting handler
 728        let fake_language_server = fake_language_servers.next().await.unwrap();
 729        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
 730            |_, _| async move {
 731                Ok(Some(vec![lsp::TextEdit {
 732                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
 733                    new_text: FORMATTED_CONTENT.to_string(),
 734                }]))
 735            }
 736        });
 737
 738        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 739        let context_server_registry =
 740            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 741        let model = Arc::new(FakeLanguageModel::default());
 742        let thread = cx.new(|cx| {
 743            Thread::new(
 744                project,
 745                cx.new(|_cx| ProjectContext::default()),
 746                context_server_registry,
 747                action_log.clone(),
 748                Templates::new(),
 749                Some(model.clone()),
 750                cx,
 751            )
 752        });
 753
 754        // First, test with format_on_save enabled
 755        cx.update(|cx| {
 756            SettingsStore::update_global(cx, |store, cx| {
 757                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 758                    cx,
 759                    |settings| {
 760                        settings.defaults.format_on_save = Some(FormatOnSave::On);
 761                        settings.defaults.formatter =
 762                            Some(language::language_settings::SelectedFormatter::Auto);
 763                    },
 764                );
 765            });
 766        });
 767
 768        // Have the model stream unformatted content
 769        let edit_result = {
 770            let edit_task = cx.update(|cx| {
 771                let input = EditFileToolInput {
 772                    display_description: "Create main function".into(),
 773                    path: "root/src/main.rs".into(),
 774                    mode: EditFileMode::Overwrite,
 775                };
 776                Arc::new(EditFileTool::new(
 777                    thread.downgrade(),
 778                    language_registry.clone(),
 779                ))
 780                .run(input, ToolCallEventStream::test().0, cx)
 781            });
 782
 783            // Stream the unformatted content
 784            cx.executor().run_until_parked();
 785            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
 786            model.end_last_completion_stream();
 787
 788            edit_task.await
 789        };
 790        assert!(edit_result.is_ok());
 791
 792        // Wait for any async operations (e.g. formatting) to complete
 793        cx.executor().run_until_parked();
 794
 795        // Read the file to verify it was formatted automatically
 796        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 797        assert_eq!(
 798            // Ignore carriage returns on Windows
 799            new_content.replace("\r\n", "\n"),
 800            FORMATTED_CONTENT,
 801            "Code should be formatted when format_on_save is enabled"
 802        );
 803
 804        let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
 805
 806        assert_eq!(
 807            stale_buffer_count, 0,
 808            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
 809             This causes the agent to think the file was modified externally when it was just formatted.",
 810            stale_buffer_count
 811        );
 812
 813        // Next, test with format_on_save disabled
 814        cx.update(|cx| {
 815            SettingsStore::update_global(cx, |store, cx| {
 816                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 817                    cx,
 818                    |settings| {
 819                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
 820                    },
 821                );
 822            });
 823        });
 824
 825        // Stream unformatted edits again
 826        let edit_result = {
 827            let edit_task = cx.update(|cx| {
 828                let input = EditFileToolInput {
 829                    display_description: "Update main function".into(),
 830                    path: "root/src/main.rs".into(),
 831                    mode: EditFileMode::Overwrite,
 832                };
 833                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 834                    input,
 835                    ToolCallEventStream::test().0,
 836                    cx,
 837                )
 838            });
 839
 840            // Stream the unformatted content
 841            cx.executor().run_until_parked();
 842            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
 843            model.end_last_completion_stream();
 844
 845            edit_task.await
 846        };
 847        assert!(edit_result.is_ok());
 848
 849        // Wait for any async operations (e.g. formatting) to complete
 850        cx.executor().run_until_parked();
 851
 852        // Verify the file was not formatted
 853        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 854        assert_eq!(
 855            // Ignore carriage returns on Windows
 856            new_content.replace("\r\n", "\n"),
 857            UNFORMATTED_CONTENT,
 858            "Code should not be formatted when format_on_save is disabled"
 859        );
 860    }
 861
 862    #[gpui::test]
 863    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
 864        init_test(cx);
 865
 866        let fs = project::FakeFs::new(cx.executor());
 867        fs.insert_tree("/root", json!({"src": {}})).await;
 868
 869        // Create a simple file with trailing whitespace
 870        fs.save(
 871            path!("/root/src/main.rs").as_ref(),
 872            &"initial content".into(),
 873            language::LineEnding::Unix,
 874        )
 875        .await
 876        .unwrap();
 877
 878        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 879        let context_server_registry =
 880            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 881        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
 882        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 883        let model = Arc::new(FakeLanguageModel::default());
 884        let thread = cx.new(|cx| {
 885            Thread::new(
 886                project,
 887                cx.new(|_cx| ProjectContext::default()),
 888                context_server_registry,
 889                action_log.clone(),
 890                Templates::new(),
 891                Some(model.clone()),
 892                cx,
 893            )
 894        });
 895
 896        // First, test with remove_trailing_whitespace_on_save enabled
 897        cx.update(|cx| {
 898            SettingsStore::update_global(cx, |store, cx| {
 899                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 900                    cx,
 901                    |settings| {
 902                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
 903                    },
 904                );
 905            });
 906        });
 907
 908        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
 909            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
 910
 911        // Have the model stream content that contains trailing whitespace
 912        let edit_result = {
 913            let edit_task = cx.update(|cx| {
 914                let input = EditFileToolInput {
 915                    display_description: "Create main function".into(),
 916                    path: "root/src/main.rs".into(),
 917                    mode: EditFileMode::Overwrite,
 918                };
 919                Arc::new(EditFileTool::new(
 920                    thread.downgrade(),
 921                    language_registry.clone(),
 922                ))
 923                .run(input, ToolCallEventStream::test().0, cx)
 924            });
 925
 926            // Stream the content with trailing whitespace
 927            cx.executor().run_until_parked();
 928            model.send_last_completion_stream_text_chunk(
 929                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
 930            );
 931            model.end_last_completion_stream();
 932
 933            edit_task.await
 934        };
 935        assert!(edit_result.is_ok());
 936
 937        // Wait for any async operations (e.g. formatting) to complete
 938        cx.executor().run_until_parked();
 939
 940        // Read the file to verify trailing whitespace was removed automatically
 941        assert_eq!(
 942            // Ignore carriage returns on Windows
 943            fs.load(path!("/root/src/main.rs").as_ref())
 944                .await
 945                .unwrap()
 946                .replace("\r\n", "\n"),
 947            "fn main() {\n    println!(\"Hello!\");\n}\n",
 948            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
 949        );
 950
 951        // Next, test with remove_trailing_whitespace_on_save disabled
 952        cx.update(|cx| {
 953            SettingsStore::update_global(cx, |store, cx| {
 954                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 955                    cx,
 956                    |settings| {
 957                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
 958                    },
 959                );
 960            });
 961        });
 962
 963        // Stream edits again with trailing whitespace
 964        let edit_result = {
 965            let edit_task = cx.update(|cx| {
 966                let input = EditFileToolInput {
 967                    display_description: "Update main function".into(),
 968                    path: "root/src/main.rs".into(),
 969                    mode: EditFileMode::Overwrite,
 970                };
 971                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 972                    input,
 973                    ToolCallEventStream::test().0,
 974                    cx,
 975                )
 976            });
 977
 978            // Stream the content with trailing whitespace
 979            cx.executor().run_until_parked();
 980            model.send_last_completion_stream_text_chunk(
 981                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
 982            );
 983            model.end_last_completion_stream();
 984
 985            edit_task.await
 986        };
 987        assert!(edit_result.is_ok());
 988
 989        // Wait for any async operations (e.g. formatting) to complete
 990        cx.executor().run_until_parked();
 991
 992        // Verify the file still has trailing whitespace
 993        // Read the file again - it should still have trailing whitespace
 994        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 995        assert_eq!(
 996            // Ignore carriage returns on Windows
 997            final_content.replace("\r\n", "\n"),
 998            CONTENT_WITH_TRAILING_WHITESPACE,
 999            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
1000        );
1001    }
1002
1003    #[gpui::test]
1004    async fn test_authorize(cx: &mut TestAppContext) {
1005        init_test(cx);
1006        let fs = project::FakeFs::new(cx.executor());
1007        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1008        let context_server_registry =
1009            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1010        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1011        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1012        let model = Arc::new(FakeLanguageModel::default());
1013        let thread = cx.new(|cx| {
1014            Thread::new(
1015                project,
1016                cx.new(|_cx| ProjectContext::default()),
1017                context_server_registry,
1018                action_log.clone(),
1019                Templates::new(),
1020                Some(model.clone()),
1021                cx,
1022            )
1023        });
1024        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1025        fs.insert_tree("/root", json!({})).await;
1026
1027        // Test 1: Path with .zed component should require confirmation
1028        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1029        let _auth = cx.update(|cx| {
1030            tool.authorize(
1031                &EditFileToolInput {
1032                    display_description: "test 1".into(),
1033                    path: ".zed/settings.json".into(),
1034                    mode: EditFileMode::Edit,
1035                },
1036                &stream_tx,
1037                cx,
1038            )
1039        });
1040
1041        let event = stream_rx.expect_authorization().await;
1042        assert_eq!(
1043            event.tool_call.fields.title,
1044            Some("test 1 (local settings)".into())
1045        );
1046
1047        // Test 2: Path outside project should require confirmation
1048        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1049        let _auth = cx.update(|cx| {
1050            tool.authorize(
1051                &EditFileToolInput {
1052                    display_description: "test 2".into(),
1053                    path: "/etc/hosts".into(),
1054                    mode: EditFileMode::Edit,
1055                },
1056                &stream_tx,
1057                cx,
1058            )
1059        });
1060
1061        let event = stream_rx.expect_authorization().await;
1062        assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
1063
1064        // Test 3: Relative path without .zed should not require confirmation
1065        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1066        cx.update(|cx| {
1067            tool.authorize(
1068                &EditFileToolInput {
1069                    display_description: "test 3".into(),
1070                    path: "root/src/main.rs".into(),
1071                    mode: EditFileMode::Edit,
1072                },
1073                &stream_tx,
1074                cx,
1075            )
1076        })
1077        .await
1078        .unwrap();
1079        assert!(stream_rx.try_next().is_err());
1080
1081        // Test 4: Path with .zed in the middle should require confirmation
1082        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1083        let _auth = cx.update(|cx| {
1084            tool.authorize(
1085                &EditFileToolInput {
1086                    display_description: "test 4".into(),
1087                    path: "root/.zed/tasks.json".into(),
1088                    mode: EditFileMode::Edit,
1089                },
1090                &stream_tx,
1091                cx,
1092            )
1093        });
1094        let event = stream_rx.expect_authorization().await;
1095        assert_eq!(
1096            event.tool_call.fields.title,
1097            Some("test 4 (local settings)".into())
1098        );
1099
1100        // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
1101        cx.update(|cx| {
1102            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1103            settings.always_allow_tool_actions = true;
1104            agent_settings::AgentSettings::override_global(settings, cx);
1105        });
1106
1107        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1108        cx.update(|cx| {
1109            tool.authorize(
1110                &EditFileToolInput {
1111                    display_description: "test 5.1".into(),
1112                    path: ".zed/settings.json".into(),
1113                    mode: EditFileMode::Edit,
1114                },
1115                &stream_tx,
1116                cx,
1117            )
1118        })
1119        .await
1120        .unwrap();
1121        assert!(stream_rx.try_next().is_err());
1122
1123        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1124        cx.update(|cx| {
1125            tool.authorize(
1126                &EditFileToolInput {
1127                    display_description: "test 5.2".into(),
1128                    path: "/etc/hosts".into(),
1129                    mode: EditFileMode::Edit,
1130                },
1131                &stream_tx,
1132                cx,
1133            )
1134        })
1135        .await
1136        .unwrap();
1137        assert!(stream_rx.try_next().is_err());
1138    }
1139
1140    #[gpui::test]
1141    async fn test_authorize_global_config(cx: &mut TestAppContext) {
1142        init_test(cx);
1143        let fs = project::FakeFs::new(cx.executor());
1144        fs.insert_tree("/project", json!({})).await;
1145        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1146        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1147        let context_server_registry =
1148            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1149        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1150        let model = Arc::new(FakeLanguageModel::default());
1151        let thread = cx.new(|cx| {
1152            Thread::new(
1153                project,
1154                cx.new(|_cx| ProjectContext::default()),
1155                context_server_registry,
1156                action_log.clone(),
1157                Templates::new(),
1158                Some(model.clone()),
1159                cx,
1160            )
1161        });
1162        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1163
1164        // Test global config paths - these should require confirmation if they exist and are outside the project
1165        let test_cases = vec![
1166            (
1167                "/etc/hosts",
1168                true,
1169                "System file should require confirmation",
1170            ),
1171            (
1172                "/usr/local/bin/script",
1173                true,
1174                "System bin file should require confirmation",
1175            ),
1176            (
1177                "project/normal_file.rs",
1178                false,
1179                "Normal project file should not require confirmation",
1180            ),
1181        ];
1182
1183        for (path, should_confirm, description) in test_cases {
1184            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1185            let auth = cx.update(|cx| {
1186                tool.authorize(
1187                    &EditFileToolInput {
1188                        display_description: "Edit file".into(),
1189                        path: path.into(),
1190                        mode: EditFileMode::Edit,
1191                    },
1192                    &stream_tx,
1193                    cx,
1194                )
1195            });
1196
1197            if should_confirm {
1198                stream_rx.expect_authorization().await;
1199            } else {
1200                auth.await.unwrap();
1201                assert!(
1202                    stream_rx.try_next().is_err(),
1203                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1204                    description,
1205                    path
1206                );
1207            }
1208        }
1209    }
1210
1211    #[gpui::test]
1212    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
1213        init_test(cx);
1214        let fs = project::FakeFs::new(cx.executor());
1215
1216        // Create multiple worktree directories
1217        fs.insert_tree(
1218            "/workspace/frontend",
1219            json!({
1220                "src": {
1221                    "main.js": "console.log('frontend');"
1222                }
1223            }),
1224        )
1225        .await;
1226        fs.insert_tree(
1227            "/workspace/backend",
1228            json!({
1229                "src": {
1230                    "main.rs": "fn main() {}"
1231                }
1232            }),
1233        )
1234        .await;
1235        fs.insert_tree(
1236            "/workspace/shared",
1237            json!({
1238                ".zed": {
1239                    "settings.json": "{}"
1240                }
1241            }),
1242        )
1243        .await;
1244
1245        // Create project with multiple worktrees
1246        let project = Project::test(
1247            fs.clone(),
1248            [
1249                path!("/workspace/frontend").as_ref(),
1250                path!("/workspace/backend").as_ref(),
1251                path!("/workspace/shared").as_ref(),
1252            ],
1253            cx,
1254        )
1255        .await;
1256        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1257        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1258        let context_server_registry =
1259            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1260        let model = Arc::new(FakeLanguageModel::default());
1261        let thread = cx.new(|cx| {
1262            Thread::new(
1263                project.clone(),
1264                cx.new(|_cx| ProjectContext::default()),
1265                context_server_registry.clone(),
1266                action_log.clone(),
1267                Templates::new(),
1268                Some(model.clone()),
1269                cx,
1270            )
1271        });
1272        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1273
1274        // Test files in different worktrees
1275        let test_cases = vec![
1276            ("frontend/src/main.js", false, "File in first worktree"),
1277            ("backend/src/main.rs", false, "File in second worktree"),
1278            (
1279                "shared/.zed/settings.json",
1280                true,
1281                ".zed file in third worktree",
1282            ),
1283            ("/etc/hosts", true, "Absolute path outside all worktrees"),
1284            (
1285                "../outside/file.txt",
1286                true,
1287                "Relative path outside worktrees",
1288            ),
1289        ];
1290
1291        for (path, should_confirm, description) in test_cases {
1292            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1293            let auth = cx.update(|cx| {
1294                tool.authorize(
1295                    &EditFileToolInput {
1296                        display_description: "Edit file".into(),
1297                        path: path.into(),
1298                        mode: EditFileMode::Edit,
1299                    },
1300                    &stream_tx,
1301                    cx,
1302                )
1303            });
1304
1305            if should_confirm {
1306                stream_rx.expect_authorization().await;
1307            } else {
1308                auth.await.unwrap();
1309                assert!(
1310                    stream_rx.try_next().is_err(),
1311                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1312                    description,
1313                    path
1314                );
1315            }
1316        }
1317    }
1318
1319    #[gpui::test]
1320    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
1321        init_test(cx);
1322        let fs = project::FakeFs::new(cx.executor());
1323        fs.insert_tree(
1324            "/project",
1325            json!({
1326                ".zed": {
1327                    "settings.json": "{}"
1328                },
1329                "src": {
1330                    ".zed": {
1331                        "local.json": "{}"
1332                    }
1333                }
1334            }),
1335        )
1336        .await;
1337        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1338        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1339        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1340        let context_server_registry =
1341            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1342        let model = Arc::new(FakeLanguageModel::default());
1343        let thread = cx.new(|cx| {
1344            Thread::new(
1345                project.clone(),
1346                cx.new(|_cx| ProjectContext::default()),
1347                context_server_registry.clone(),
1348                action_log.clone(),
1349                Templates::new(),
1350                Some(model.clone()),
1351                cx,
1352            )
1353        });
1354        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1355
1356        // Test edge cases
1357        let test_cases = vec![
1358            // Empty path - find_project_path returns Some for empty paths
1359            ("", false, "Empty path is treated as project root"),
1360            // Root directory
1361            ("/", true, "Root directory should be outside project"),
1362            // Parent directory references - find_project_path resolves these
1363            (
1364                "project/../other",
1365                false,
1366                "Path with .. is resolved by find_project_path",
1367            ),
1368            (
1369                "project/./src/file.rs",
1370                false,
1371                "Path with . should work normally",
1372            ),
1373            // Windows-style paths (if on Windows)
1374            #[cfg(target_os = "windows")]
1375            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
1376            #[cfg(target_os = "windows")]
1377            ("project\\src\\main.rs", false, "Windows-style project path"),
1378        ];
1379
1380        for (path, should_confirm, description) in test_cases {
1381            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1382            let auth = cx.update(|cx| {
1383                tool.authorize(
1384                    &EditFileToolInput {
1385                        display_description: "Edit file".into(),
1386                        path: path.into(),
1387                        mode: EditFileMode::Edit,
1388                    },
1389                    &stream_tx,
1390                    cx,
1391                )
1392            });
1393
1394            if should_confirm {
1395                stream_rx.expect_authorization().await;
1396            } else {
1397                auth.await.unwrap();
1398                assert!(
1399                    stream_rx.try_next().is_err(),
1400                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1401                    description,
1402                    path
1403                );
1404            }
1405        }
1406    }
1407
1408    #[gpui::test]
1409    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
1410        init_test(cx);
1411        let fs = project::FakeFs::new(cx.executor());
1412        fs.insert_tree(
1413            "/project",
1414            json!({
1415                "existing.txt": "content",
1416                ".zed": {
1417                    "settings.json": "{}"
1418                }
1419            }),
1420        )
1421        .await;
1422        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1423        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1424        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1425        let context_server_registry =
1426            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1427        let model = Arc::new(FakeLanguageModel::default());
1428        let thread = cx.new(|cx| {
1429            Thread::new(
1430                project.clone(),
1431                cx.new(|_cx| ProjectContext::default()),
1432                context_server_registry.clone(),
1433                action_log.clone(),
1434                Templates::new(),
1435                Some(model.clone()),
1436                cx,
1437            )
1438        });
1439        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1440
1441        // Test different EditFileMode values
1442        let modes = vec![
1443            EditFileMode::Edit,
1444            EditFileMode::Create,
1445            EditFileMode::Overwrite,
1446        ];
1447
1448        for mode in modes {
1449            // Test .zed path with different modes
1450            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1451            let _auth = cx.update(|cx| {
1452                tool.authorize(
1453                    &EditFileToolInput {
1454                        display_description: "Edit settings".into(),
1455                        path: "project/.zed/settings.json".into(),
1456                        mode: mode.clone(),
1457                    },
1458                    &stream_tx,
1459                    cx,
1460                )
1461            });
1462
1463            stream_rx.expect_authorization().await;
1464
1465            // Test outside path with different modes
1466            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1467            let _auth = cx.update(|cx| {
1468                tool.authorize(
1469                    &EditFileToolInput {
1470                        display_description: "Edit file".into(),
1471                        path: "/outside/file.txt".into(),
1472                        mode: mode.clone(),
1473                    },
1474                    &stream_tx,
1475                    cx,
1476                )
1477            });
1478
1479            stream_rx.expect_authorization().await;
1480
1481            // Test normal path with different modes
1482            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1483            cx.update(|cx| {
1484                tool.authorize(
1485                    &EditFileToolInput {
1486                        display_description: "Edit file".into(),
1487                        path: "project/normal.txt".into(),
1488                        mode: mode.clone(),
1489                    },
1490                    &stream_tx,
1491                    cx,
1492                )
1493            })
1494            .await
1495            .unwrap();
1496            assert!(stream_rx.try_next().is_err());
1497        }
1498    }
1499
1500    #[gpui::test]
1501    async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
1502        init_test(cx);
1503        let fs = project::FakeFs::new(cx.executor());
1504        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1505        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1506        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1507        let context_server_registry =
1508            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1509        let model = Arc::new(FakeLanguageModel::default());
1510        let thread = cx.new(|cx| {
1511            Thread::new(
1512                project.clone(),
1513                cx.new(|_cx| ProjectContext::default()),
1514                context_server_registry,
1515                action_log.clone(),
1516                Templates::new(),
1517                Some(model.clone()),
1518                cx,
1519            )
1520        });
1521        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1522
1523        assert_eq!(
1524            tool.initial_title(Err(json!({
1525                "path": "src/main.rs",
1526                "display_description": "",
1527                "old_string": "old code",
1528                "new_string": "new code"
1529            }))),
1530            "src/main.rs"
1531        );
1532        assert_eq!(
1533            tool.initial_title(Err(json!({
1534                "path": "",
1535                "display_description": "Fix error handling",
1536                "old_string": "old code",
1537                "new_string": "new code"
1538            }))),
1539            "Fix error handling"
1540        );
1541        assert_eq!(
1542            tool.initial_title(Err(json!({
1543                "path": "src/main.rs",
1544                "display_description": "Fix error handling",
1545                "old_string": "old code",
1546                "new_string": "new code"
1547            }))),
1548            "Fix error handling"
1549        );
1550        assert_eq!(
1551            tool.initial_title(Err(json!({
1552                "path": "",
1553                "display_description": "",
1554                "old_string": "old code",
1555                "new_string": "new code"
1556            }))),
1557            DEFAULT_UI_TEXT
1558        );
1559        assert_eq!(
1560            tool.initial_title(Err(serde_json::Value::Null)),
1561            DEFAULT_UI_TEXT
1562        );
1563    }
1564
1565    fn init_test(cx: &mut TestAppContext) {
1566        cx.update(|cx| {
1567            let settings_store = SettingsStore::test(cx);
1568            cx.set_global(settings_store);
1569            language::init(cx);
1570            TelemetrySettings::register(cx);
1571            agent_settings::AgentSettings::register(cx);
1572            Project::init_settings(cx);
1573        });
1574    }
1575}