edit_file_tool.rs

   1use crate::{
   2    Templates,
   3    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
   4    schema::json_schema_for,
   5    ui::{COLLAPSED_LINES, ToolOutputPreview},
   6};
   7use agent_settings;
   8use anyhow::{Context as _, Result, anyhow};
   9use assistant_tool::{
  10    ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
  11    ToolUseStatus,
  12};
  13use buffer_diff::{BufferDiff, BufferDiffSnapshot};
  14use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
  15use futures::StreamExt;
  16use gpui::{
  17    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
  18    TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
  19};
  20use indoc::formatdoc;
  21use language::{
  22    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
  23    TextBuffer,
  24    language_settings::{self, FormatOnSave, SoftWrap},
  25};
  26use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
  27use markdown::{Markdown, MarkdownElement, MarkdownStyle};
  28use project::{
  29    Project, ProjectPath,
  30    lsp_store::{FormatTrigger, LspFormatTarget},
  31};
  32use schemars::JsonSchema;
  33use serde::{Deserialize, Serialize};
  34use settings::Settings;
  35use std::{
  36    cmp::Reverse,
  37    collections::HashSet,
  38    ops::Range,
  39    path::{Path, PathBuf},
  40    sync::Arc,
  41    time::Duration,
  42};
  43use theme::ThemeSettings;
  44use ui::{Disclosure, Tooltip, prelude::*};
  45use util::ResultExt;
  46use workspace::Workspace;
  47
  48pub struct EditFileTool;
  49
  50#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  51pub struct EditFileToolInput {
  52    /// A one-line, user-friendly markdown description of the edit. This will be
  53    /// shown in the UI and also passed to another model to perform the edit.
  54    ///
  55    /// Be terse, but also descriptive in what you want to achieve with this
  56    /// edit. Avoid generic instructions.
  57    ///
  58    /// NEVER mention the file path in this description.
  59    ///
  60    /// <example>Fix API endpoint URLs</example>
  61    /// <example>Update copyright year in `page_footer`</example>
  62    ///
  63    /// Make sure to include this field before all the others in the input object
  64    /// so that we can display it immediately.
  65    pub display_description: String,
  66
  67    /// The full path of the file to create or modify in the project.
  68    ///
  69    /// WARNING: When specifying which file path need changing, you MUST
  70    /// start each path with one of the project's root directories.
  71    ///
  72    /// The following examples assume we have two root directories in the project:
  73    /// - /a/b/backend
  74    /// - /c/d/frontend
  75    ///
  76    /// <example>
  77    /// `backend/src/main.rs`
  78    ///
  79    /// Notice how the file path starts with `backend`. Without that, the path
  80    /// would be ambiguous and the call would fail!
  81    /// </example>
  82    ///
  83    /// <example>
  84    /// `frontend/db.js`
  85    /// </example>
  86    pub path: PathBuf,
  87
  88    /// The mode of operation on the file. Possible values:
  89    /// - 'edit': Make granular edits to an existing file.
  90    /// - 'create': Create a new file if it doesn't exist.
  91    /// - 'overwrite': Replace the entire contents of an existing file.
  92    ///
  93    /// When a file already exists or you just created it, prefer editing
  94    /// it as opposed to recreating it from scratch.
  95    pub mode: EditFileMode,
  96}
  97
  98#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  99#[serde(rename_all = "lowercase")]
 100pub enum EditFileMode {
 101    Edit,
 102    Create,
 103    Overwrite,
 104}
 105
 106#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 107pub struct EditFileToolOutput {
 108    pub original_path: PathBuf,
 109    pub new_text: String,
 110    pub old_text: Arc<String>,
 111    pub raw_output: Option<EditAgentOutput>,
 112}
 113
 114#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 115struct PartialInput {
 116    #[serde(default)]
 117    path: String,
 118    #[serde(default)]
 119    display_description: String,
 120}
 121
 122const DEFAULT_UI_TEXT: &str = "Editing file";
 123
 124impl Tool for EditFileTool {
 125    fn name(&self) -> String {
 126        "edit_file".into()
 127    }
 128
 129    fn needs_confirmation(
 130        &self,
 131        input: &serde_json::Value,
 132        project: &Entity<Project>,
 133        cx: &App,
 134    ) -> bool {
 135        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
 136            return false;
 137        }
 138
 139        let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
 140            // If it's not valid JSON, it's going to error and confirming won't do anything.
 141            return false;
 142        };
 143
 144        let path = Path::new(&input.path);
 145
 146        // If any path component is ".zed", then this could affect
 147        // the editor in ways beyond the project source, so prompt.
 148        if path
 149            .components()
 150            .any(|component| component.as_os_str() == ".zed")
 151        {
 152            return true;
 153        }
 154
 155        // If the path is outside the project, then prompt.
 156        let is_outside_project = project
 157            .read(cx)
 158            .find_project_path(&input.path, cx)
 159            .is_none();
 160        if is_outside_project {
 161            return true;
 162        }
 163
 164        false
 165    }
 166
 167    fn may_perform_edits(&self) -> bool {
 168        true
 169    }
 170
 171    fn description(&self) -> String {
 172        include_str!("edit_file_tool/description.md").to_string()
 173    }
 174
 175    fn icon(&self) -> IconName {
 176        IconName::ToolPencil
 177    }
 178
 179    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
 180        json_schema_for::<EditFileToolInput>(format)
 181    }
 182
 183    fn ui_text(&self, input: &serde_json::Value) -> String {
 184        match serde_json::from_value::<EditFileToolInput>(input.clone()) {
 185            Ok(input) => {
 186                let path = Path::new(&input.path);
 187                let mut description = input.display_description.clone();
 188
 189                // Add context about why confirmation may be needed
 190                if path.components().any(|c| c.as_os_str() == ".zed") {
 191                    description.push_str(" (Zed settings)");
 192                }
 193
 194                description
 195            }
 196            Err(_) => "Editing file".to_string(),
 197        }
 198    }
 199
 200    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
 201        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
 202            let description = input.display_description.trim();
 203            if !description.is_empty() {
 204                return description.to_string();
 205            }
 206
 207            let path = input.path.trim();
 208            if !path.is_empty() {
 209                return path.to_string();
 210            }
 211        }
 212
 213        DEFAULT_UI_TEXT.to_string()
 214    }
 215
 216    fn run(
 217        self: Arc<Self>,
 218        input: serde_json::Value,
 219        request: Arc<LanguageModelRequest>,
 220        project: Entity<Project>,
 221        action_log: Entity<ActionLog>,
 222        model: Arc<dyn LanguageModel>,
 223        window: Option<AnyWindowHandle>,
 224        cx: &mut App,
 225    ) -> ToolResult {
 226        let input = match serde_json::from_value::<EditFileToolInput>(input) {
 227            Ok(input) => input,
 228            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
 229        };
 230
 231        let project_path = match resolve_path(&input, project.clone(), cx) {
 232            Ok(path) => path,
 233            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
 234        };
 235
 236        let card = window.and_then(|window| {
 237            window
 238                .update(cx, |_, window, cx| {
 239                    cx.new(|cx| {
 240                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
 241                    })
 242                })
 243                .ok()
 244        });
 245
 246        let card_clone = card.clone();
 247        let action_log_clone = action_log.clone();
 248        let task = cx.spawn(async move |cx: &mut AsyncApp| {
 249            let edit_format = EditFormat::from_model(model.clone())?;
 250            let edit_agent = EditAgent::new(
 251                model,
 252                project.clone(),
 253                action_log_clone,
 254                Templates::new(),
 255                edit_format,
 256            );
 257
 258            let buffer = project
 259                .update(cx, |project, cx| {
 260                    project.open_buffer(project_path.clone(), cx)
 261                })?
 262                .await?;
 263
 264            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 265            let old_text = cx
 266                .background_spawn({
 267                    let old_snapshot = old_snapshot.clone();
 268                    async move { Arc::new(old_snapshot.text()) }
 269                })
 270                .await;
 271
 272            if let Some(card) = card_clone.as_ref() {
 273                card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;
 274            }
 275
 276            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
 277                edit_agent.edit(
 278                    buffer.clone(),
 279                    input.display_description.clone(),
 280                    &request,
 281                    cx,
 282                )
 283            } else {
 284                edit_agent.overwrite(
 285                    buffer.clone(),
 286                    input.display_description.clone(),
 287                    &request,
 288                    cx,
 289                )
 290            };
 291
 292            let mut hallucinated_old_text = false;
 293            let mut ambiguous_ranges = Vec::new();
 294            while let Some(event) = events.next().await {
 295                match event {
 296                    EditAgentOutputEvent::Edited => {
 297                        if let Some(card) = card_clone.as_ref() {
 298                            card.update(cx, |card, cx| card.update_diff(cx))?;
 299                        }
 300                    }
 301                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
 302                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
 303                    EditAgentOutputEvent::ResolvingEditRange(range) => {
 304                        if let Some(card) = card_clone.as_ref() {
 305                            card.update(cx, |card, cx| card.reveal_range(range, cx))?;
 306                        }
 307                    }
 308                }
 309            }
 310            let agent_output = output.await?;
 311
 312            // If format_on_save is enabled, format the buffer
 313            let format_on_save_enabled = buffer
 314                .read_with(cx, |buffer, cx| {
 315                    let settings = language_settings::language_settings(
 316                        buffer.language().map(|l| l.name()),
 317                        buffer.file(),
 318                        cx,
 319                    );
 320                    !matches!(settings.format_on_save, FormatOnSave::Off)
 321                })
 322                .unwrap_or(false);
 323
 324            if format_on_save_enabled {
 325                action_log.update(cx, |log, cx| {
 326                    log.buffer_edited(buffer.clone(), cx);
 327                })?;
 328                let format_task = project.update(cx, |project, cx| {
 329                    project.format(
 330                        HashSet::from_iter([buffer.clone()]),
 331                        LspFormatTarget::Buffers,
 332                        false, // Don't push to history since the tool did it.
 333                        FormatTrigger::Save,
 334                        cx,
 335                    )
 336                })?;
 337                format_task.await.log_err();
 338            }
 339
 340            project
 341                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
 342                .await?;
 343
 344            // Notify the action log that we've edited the buffer (*after* formatting has completed).
 345            action_log.update(cx, |log, cx| {
 346                log.buffer_edited(buffer.clone(), cx);
 347            })?;
 348
 349            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 350            let (new_text, diff) = cx
 351                .background_spawn({
 352                    let new_snapshot = new_snapshot.clone();
 353                    let old_text = old_text.clone();
 354                    async move {
 355                        let new_text = new_snapshot.text();
 356                        let diff = language::unified_diff(&old_text, &new_text);
 357
 358                        (new_text, diff)
 359                    }
 360                })
 361                .await;
 362
 363            let output = EditFileToolOutput {
 364                original_path: project_path.path.to_path_buf(),
 365                new_text: new_text.clone(),
 366                old_text,
 367                raw_output: Some(agent_output),
 368            };
 369
 370            if let Some(card) = card_clone {
 371                card.update(cx, |card, cx| {
 372                    card.update_diff(cx);
 373                    card.finalize(cx)
 374                })
 375                .log_err();
 376            }
 377
 378            let input_path = input.path.display();
 379            if diff.is_empty() {
 380                anyhow::ensure!(
 381                    !hallucinated_old_text,
 382                    formatdoc! {"
 383                        Some edits were produced but none of them could be applied.
 384                        Read the relevant sections of {input_path} again so that
 385                        I can perform the requested edits.
 386                    "}
 387                );
 388                anyhow::ensure!(
 389                    ambiguous_ranges.is_empty(),
 390                    {
 391                        let line_numbers = ambiguous_ranges
 392                            .iter()
 393                            .map(|range| range.start.to_string())
 394                            .collect::<Vec<_>>()
 395                            .join(", ");
 396                        formatdoc! {"
 397                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
 398                            relevant sections of {input_path} again and extend <old_text> so
 399                            that I can perform the requested edits.
 400                        "}
 401                    }
 402                );
 403                Ok(ToolResultOutput {
 404                    content: ToolResultContent::Text("No edits were made.".into()),
 405                    output: serde_json::to_value(output).ok(),
 406                })
 407            } else {
 408                Ok(ToolResultOutput {
 409                    content: ToolResultContent::Text(format!(
 410                        "Edited {}:\n\n```diff\n{}\n```",
 411                        input_path, diff
 412                    )),
 413                    output: serde_json::to_value(output).ok(),
 414                })
 415            }
 416        });
 417
 418        ToolResult {
 419            output: task,
 420            card: card.map(AnyToolCard::from),
 421        }
 422    }
 423
 424    fn deserialize_card(
 425        self: Arc<Self>,
 426        output: serde_json::Value,
 427        project: Entity<Project>,
 428        window: &mut Window,
 429        cx: &mut App,
 430    ) -> Option<AnyToolCard> {
 431        let output = match serde_json::from_value::<EditFileToolOutput>(output) {
 432            Ok(output) => output,
 433            Err(_) => return None,
 434        };
 435
 436        let card = cx.new(|cx| {
 437            EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)
 438        });
 439
 440        cx.spawn({
 441            let path: Arc<Path> = output.original_path.into();
 442            let language_registry = project.read(cx).languages().clone();
 443            let card = card.clone();
 444            async move |cx| {
 445                let buffer =
 446                    build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;
 447                let buffer_diff =
 448                    build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)
 449                        .await?;
 450                card.update(cx, |card, cx| {
 451                    card.multibuffer.update(cx, |multibuffer, cx| {
 452                        let snapshot = buffer.read(cx).snapshot();
 453                        let diff = buffer_diff.read(cx);
 454                        let diff_hunk_ranges = diff
 455                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
 456                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
 457                            .collect::<Vec<_>>();
 458
 459                        multibuffer.set_excerpts_for_path(
 460                            PathKey::for_buffer(&buffer, cx),
 461                            buffer,
 462                            diff_hunk_ranges,
 463                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
 464                            cx,
 465                        );
 466                        multibuffer.add_diff(buffer_diff, cx);
 467                        let end = multibuffer.len(cx);
 468                        card.total_lines =
 469                            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);
 470                    });
 471
 472                    cx.notify();
 473                })?;
 474                anyhow::Ok(())
 475            }
 476        })
 477        .detach_and_log_err(cx);
 478
 479        Some(card.into())
 480    }
 481}
 482
 483/// Validate that the file path is valid, meaning:
 484///
 485/// - For `edit` and `overwrite`, the path must point to an existing file.
 486/// - For `create`, the file must not already exist, but it's parent dir must exist.
 487fn resolve_path(
 488    input: &EditFileToolInput,
 489    project: Entity<Project>,
 490    cx: &mut App,
 491) -> Result<ProjectPath> {
 492    let project = project.read(cx);
 493
 494    match input.mode {
 495        EditFileMode::Edit | EditFileMode::Overwrite => {
 496            let path = project
 497                .find_project_path(&input.path, cx)
 498                .context("Can't edit file: path not found")?;
 499
 500            let entry = project
 501                .entry_for_path(&path, cx)
 502                .context("Can't edit file: path not found")?;
 503
 504            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
 505            Ok(path)
 506        }
 507
 508        EditFileMode::Create => {
 509            if let Some(path) = project.find_project_path(&input.path, cx) {
 510                anyhow::ensure!(
 511                    project.entry_for_path(&path, cx).is_none(),
 512                    "Can't create file: file already exists"
 513                );
 514            }
 515
 516            let parent_path = input
 517                .path
 518                .parent()
 519                .context("Can't create file: incorrect path")?;
 520
 521            let parent_project_path = project.find_project_path(&parent_path, cx);
 522
 523            let parent_entry = parent_project_path
 524                .as_ref()
 525                .and_then(|path| project.entry_for_path(&path, cx))
 526                .context("Can't create file: parent directory doesn't exist")?;
 527
 528            anyhow::ensure!(
 529                parent_entry.is_dir(),
 530                "Can't create file: parent is not a directory"
 531            );
 532
 533            let file_name = input
 534                .path
 535                .file_name()
 536                .context("Can't create file: invalid filename")?;
 537
 538            let new_file_path = parent_project_path.map(|parent| ProjectPath {
 539                path: Arc::from(parent.path.join(file_name)),
 540                ..parent
 541            });
 542
 543            new_file_path.context("Can't create file")
 544        }
 545    }
 546}
 547
 548pub struct EditFileToolCard {
 549    path: PathBuf,
 550    editor: Entity<Editor>,
 551    multibuffer: Entity<MultiBuffer>,
 552    project: Entity<Project>,
 553    buffer: Option<Entity<Buffer>>,
 554    base_text: Option<Arc<String>>,
 555    buffer_diff: Option<Entity<BufferDiff>>,
 556    revealed_ranges: Vec<Range<Anchor>>,
 557    diff_task: Option<Task<Result<()>>>,
 558    preview_expanded: bool,
 559    error_expanded: Option<Entity<Markdown>>,
 560    full_height_expanded: bool,
 561    total_lines: Option<u32>,
 562}
 563
 564impl EditFileToolCard {
 565    pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
 566        let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
 567        let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
 568
 569        let editor = cx.new(|cx| {
 570            let mut editor = Editor::new(
 571                EditorMode::Full {
 572                    scale_ui_elements_with_buffer_font_size: false,
 573                    show_active_line_background: false,
 574                    sized_by_content: true,
 575                },
 576                multibuffer.clone(),
 577                Some(project.clone()),
 578                window,
 579                cx,
 580            );
 581            editor.set_show_gutter(false, cx);
 582            editor.disable_inline_diagnostics();
 583            editor.disable_expand_excerpt_buttons(cx);
 584            // Keep horizontal scrollbar so user can scroll horizontally if needed
 585            editor.set_show_vertical_scrollbar(false, cx);
 586            editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 587            editor.set_soft_wrap_mode(SoftWrap::None, cx);
 588            editor.scroll_manager.set_forbid_vertical_scroll(true);
 589            editor.set_show_indent_guides(false, cx);
 590            editor.set_read_only(true);
 591            editor.set_show_breakpoints(false, cx);
 592            editor.set_show_code_actions(false, cx);
 593            editor.set_show_git_diff_gutter(false, cx);
 594            editor.set_expand_all_diff_hunks(cx);
 595            editor
 596        });
 597        Self {
 598            path,
 599            project,
 600            editor,
 601            multibuffer,
 602            buffer: None,
 603            base_text: None,
 604            buffer_diff: None,
 605            revealed_ranges: Vec::new(),
 606            diff_task: None,
 607            preview_expanded: true,
 608            error_expanded: None,
 609            full_height_expanded: expand_edit_card,
 610            total_lines: None,
 611        }
 612    }
 613
 614    pub fn initialize(&mut self, buffer: Entity<Buffer>, cx: &mut App) {
 615        let buffer_snapshot = buffer.read(cx).snapshot();
 616        let base_text = buffer_snapshot.text();
 617        let language_registry = buffer.read(cx).language_registry();
 618        let text_snapshot = buffer.read(cx).text_snapshot();
 619
 620        // Create a buffer diff with the current text as the base
 621        let buffer_diff = cx.new(|cx| {
 622            let mut diff = BufferDiff::new(&text_snapshot, cx);
 623            let _ = diff.set_base_text(
 624                buffer_snapshot.clone(),
 625                language_registry,
 626                text_snapshot,
 627                cx,
 628            );
 629            diff
 630        });
 631
 632        self.buffer = Some(buffer.clone());
 633        self.base_text = Some(base_text.into());
 634        self.buffer_diff = Some(buffer_diff.clone());
 635
 636        // Add the diff to the multibuffer
 637        self.multibuffer
 638            .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx));
 639    }
 640
 641    pub fn is_loading(&self) -> bool {
 642        self.total_lines.is_none()
 643    }
 644
 645    pub fn update_diff(&mut self, cx: &mut Context<Self>) {
 646        let Some(buffer) = self.buffer.as_ref() else {
 647            return;
 648        };
 649        let Some(buffer_diff) = self.buffer_diff.as_ref() else {
 650            return;
 651        };
 652
 653        let buffer = buffer.clone();
 654        let buffer_diff = buffer_diff.clone();
 655        let base_text = self.base_text.clone();
 656        self.diff_task = Some(cx.spawn(async move |this, cx| {
 657            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
 658            let diff_snapshot = BufferDiff::update_diff(
 659                buffer_diff.clone(),
 660                text_snapshot.clone(),
 661                base_text,
 662                false,
 663                false,
 664                None,
 665                None,
 666                cx,
 667            )
 668            .await?;
 669            buffer_diff.update(cx, |diff, cx| {
 670                diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
 671            })?;
 672            this.update(cx, |this, cx| this.update_visible_ranges(cx))
 673        }));
 674    }
 675
 676    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
 677        self.revealed_ranges.push(range);
 678        self.update_visible_ranges(cx);
 679    }
 680
 681    fn update_visible_ranges(&mut self, cx: &mut Context<Self>) {
 682        let Some(buffer) = self.buffer.as_ref() else {
 683            return;
 684        };
 685
 686        let ranges = self.excerpt_ranges(cx);
 687        self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| {
 688            multibuffer.set_excerpts_for_path(
 689                PathKey::for_buffer(buffer, cx),
 690                buffer.clone(),
 691                ranges,
 692                editor::DEFAULT_MULTIBUFFER_CONTEXT,
 693                cx,
 694            );
 695            let end = multibuffer.len(cx);
 696            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
 697        });
 698        cx.notify();
 699    }
 700
 701    fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
 702        let Some(buffer) = self.buffer.as_ref() else {
 703            return Vec::new();
 704        };
 705        let Some(diff) = self.buffer_diff.as_ref() else {
 706            return Vec::new();
 707        };
 708
 709        let buffer = buffer.read(cx);
 710        let diff = diff.read(cx);
 711        let mut ranges = diff
 712            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
 713            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
 714            .collect::<Vec<_>>();
 715        ranges.extend(
 716            self.revealed_ranges
 717                .iter()
 718                .map(|range| range.to_point(&buffer)),
 719        );
 720        ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
 721
 722        // Merge adjacent ranges
 723        let mut ranges = ranges.into_iter().peekable();
 724        let mut merged_ranges = Vec::new();
 725        while let Some(mut range) = ranges.next() {
 726            while let Some(next_range) = ranges.peek() {
 727                if range.end >= next_range.start {
 728                    range.end = range.end.max(next_range.end);
 729                    ranges.next();
 730                } else {
 731                    break;
 732                }
 733            }
 734
 735            merged_ranges.push(range);
 736        }
 737        merged_ranges
 738    }
 739
 740    pub fn finalize(&mut self, cx: &mut Context<Self>) -> Result<()> {
 741        let ranges = self.excerpt_ranges(cx);
 742        let buffer = self.buffer.take().context("card was already finalized")?;
 743        let base_text = self
 744            .base_text
 745            .take()
 746            .context("card was already finalized")?;
 747        let language_registry = self.project.read(cx).languages().clone();
 748
 749        // Replace the buffer in the multibuffer with the snapshot
 750        let buffer = cx.new(|cx| {
 751            let language = buffer.read(cx).language().cloned();
 752            let buffer = TextBuffer::new_normalized(
 753                0,
 754                cx.entity_id().as_non_zero_u64().into(),
 755                buffer.read(cx).line_ending(),
 756                buffer.read(cx).as_rope().clone(),
 757            );
 758            let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
 759            buffer.set_language(language, cx);
 760            buffer
 761        });
 762
 763        let buffer_diff = cx.spawn({
 764            let buffer = buffer.clone();
 765            let language_registry = language_registry.clone();
 766            async move |_this, cx| {
 767                build_buffer_diff(base_text, &buffer, &language_registry, cx).await
 768            }
 769        });
 770
 771        cx.spawn(async move |this, cx| {
 772            let buffer_diff = buffer_diff.await?;
 773            this.update(cx, |this, cx| {
 774                this.multibuffer.update(cx, |multibuffer, cx| {
 775                    let path_key = PathKey::for_buffer(&buffer, cx);
 776                    multibuffer.clear(cx);
 777                    multibuffer.set_excerpts_for_path(
 778                        path_key,
 779                        buffer,
 780                        ranges,
 781                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
 782                        cx,
 783                    );
 784                    multibuffer.add_diff(buffer_diff.clone(), cx);
 785                });
 786
 787                cx.notify();
 788            })
 789        })
 790        .detach_and_log_err(cx);
 791        Ok(())
 792    }
 793}
 794
 795impl ToolCard for EditFileToolCard {
 796    fn render(
 797        &mut self,
 798        status: &ToolUseStatus,
 799        window: &mut Window,
 800        workspace: WeakEntity<Workspace>,
 801        cx: &mut Context<Self>,
 802    ) -> impl IntoElement {
 803        let error_message = match status {
 804            ToolUseStatus::Error(err) => Some(err),
 805            _ => None,
 806        };
 807
 808        let running_or_pending = match status {
 809            ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
 810            _ => None,
 811        };
 812
 813        let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
 814
 815        let path_label_button = h_flex()
 816            .id(("edit-tool-path-label-button", self.editor.entity_id()))
 817            .w_full()
 818            .max_w_full()
 819            .px_1()
 820            .gap_0p5()
 821            .cursor_pointer()
 822            .rounded_sm()
 823            .opacity(0.8)
 824            .hover(|label| {
 825                label
 826                    .opacity(1.)
 827                    .bg(cx.theme().colors().element_hover.opacity(0.5))
 828            })
 829            .tooltip(Tooltip::text("Jump to File"))
 830            .child(
 831                h_flex()
 832                    .child(
 833                        Icon::new(IconName::ToolPencil)
 834                            .size(IconSize::Small)
 835                            .color(Color::Muted),
 836                    )
 837                    .child(
 838                        div()
 839                            .text_size(rems(0.8125))
 840                            .child(self.path.display().to_string())
 841                            .ml_1p5()
 842                            .mr_0p5(),
 843                    )
 844                    .child(
 845                        Icon::new(IconName::ArrowUpRight)
 846                            .size(IconSize::XSmall)
 847                            .color(Color::Ignored),
 848                    ),
 849            )
 850            .on_click({
 851                let path = self.path.clone();
 852                let workspace = workspace.clone();
 853                move |_, window, cx| {
 854                    workspace
 855                        .update(cx, {
 856                            |workspace, cx| {
 857                                let Some(project_path) =
 858                                    workspace.project().read(cx).find_project_path(&path, cx)
 859                                else {
 860                                    return;
 861                                };
 862                                let open_task =
 863                                    workspace.open_path(project_path, None, true, window, cx);
 864                                window
 865                                    .spawn(cx, async move |cx| {
 866                                        let item = open_task.await?;
 867                                        if let Some(active_editor) = item.downcast::<Editor>() {
 868                                            active_editor
 869                                                .update_in(cx, |editor, window, cx| {
 870                                                    let snapshot =
 871                                                        editor.buffer().read(cx).snapshot(cx);
 872                                                    let first_hunk = editor
 873                                                        .diff_hunks_in_ranges(
 874                                                            &[editor::Anchor::min()
 875                                                                ..editor::Anchor::max()],
 876                                                            &snapshot,
 877                                                        )
 878                                                        .next();
 879                                                    if let Some(first_hunk) = first_hunk {
 880                                                        let first_hunk_start =
 881                                                            first_hunk.multi_buffer_range().start;
 882                                                        editor.change_selections(
 883                                                            Default::default(),
 884                                                            window,
 885                                                            cx,
 886                                                            |selections| {
 887                                                                selections.select_anchor_ranges([
 888                                                                    first_hunk_start
 889                                                                        ..first_hunk_start,
 890                                                                ]);
 891                                                            },
 892                                                        )
 893                                                    }
 894                                                })
 895                                                .log_err();
 896                                        }
 897                                        anyhow::Ok(())
 898                                    })
 899                                    .detach_and_log_err(cx);
 900                            }
 901                        })
 902                        .ok();
 903                }
 904            })
 905            .into_any_element();
 906
 907        let codeblock_header_bg = cx
 908            .theme()
 909            .colors()
 910            .element_background
 911            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
 912
 913        let codeblock_header = h_flex()
 914            .flex_none()
 915            .p_1()
 916            .gap_1()
 917            .justify_between()
 918            .rounded_t_md()
 919            .when(error_message.is_none(), |header| {
 920                header.bg(codeblock_header_bg)
 921            })
 922            .child(path_label_button)
 923            .when(should_show_loading, |header| {
 924                header.pr_1p5().child(
 925                    Icon::new(IconName::ArrowCircle)
 926                        .size(IconSize::XSmall)
 927                        .color(Color::Info)
 928                        .with_animation(
 929                            "arrow-circle",
 930                            Animation::new(Duration::from_secs(2)).repeat(),
 931                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 932                        ),
 933                )
 934            })
 935            .when_some(error_message, |header, error_message| {
 936                header.child(
 937                    h_flex()
 938                        .gap_1()
 939                        .child(
 940                            Icon::new(IconName::Close)
 941                                .size(IconSize::Small)
 942                                .color(Color::Error),
 943                        )
 944                        .child(
 945                            Disclosure::new(
 946                                ("edit-file-error-disclosure", self.editor.entity_id()),
 947                                self.error_expanded.is_some(),
 948                            )
 949                            .opened_icon(IconName::ChevronUp)
 950                            .closed_icon(IconName::ChevronDown)
 951                            .on_click(cx.listener({
 952                                let error_message = error_message.clone();
 953
 954                                move |this, _event, _window, cx| {
 955                                    if this.error_expanded.is_some() {
 956                                        this.error_expanded.take();
 957                                    } else {
 958                                        this.error_expanded = Some(cx.new(|cx| {
 959                                            Markdown::new(error_message.clone(), None, None, cx)
 960                                        }))
 961                                    }
 962                                    cx.notify();
 963                                }
 964                            })),
 965                        ),
 966                )
 967            })
 968            .when(error_message.is_none() && !self.is_loading(), |header| {
 969                header.child(
 970                    Disclosure::new(
 971                        ("edit-file-disclosure", self.editor.entity_id()),
 972                        self.preview_expanded,
 973                    )
 974                    .opened_icon(IconName::ChevronUp)
 975                    .closed_icon(IconName::ChevronDown)
 976                    .on_click(cx.listener(
 977                        move |this, _event, _window, _cx| {
 978                            this.preview_expanded = !this.preview_expanded;
 979                        },
 980                    )),
 981                )
 982            });
 983
 984        let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
 985            let line_height = editor
 986                .style()
 987                .map(|style| style.text.line_height_in_pixels(window.rem_size()))
 988                .unwrap_or_default();
 989
 990            editor.set_text_style_refinement(TextStyleRefinement {
 991                font_size: Some(
 992                    TextSize::Small
 993                        .rems(cx)
 994                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
 995                        .into(),
 996                ),
 997                ..TextStyleRefinement::default()
 998            });
 999            let element = editor.render(window, cx);
1000            (element.into_any_element(), line_height)
1001        });
1002
1003        let border_color = cx.theme().colors().border.opacity(0.6);
1004
1005        let waiting_for_diff = {
1006            let styles = [
1007                ("w_4_5", (0.1, 0.85), 2000),
1008                ("w_1_4", (0.2, 0.75), 2200),
1009                ("w_2_4", (0.15, 0.64), 1900),
1010                ("w_3_5", (0.25, 0.72), 2300),
1011                ("w_2_5", (0.3, 0.56), 1800),
1012            ];
1013
1014            let mut container = v_flex()
1015                .p_3()
1016                .gap_1()
1017                .border_t_1()
1018                .rounded_b_md()
1019                .border_color(border_color)
1020                .bg(cx.theme().colors().editor_background);
1021
1022            for (width_method, pulse_range, duration_ms) in styles.iter() {
1023                let (min_opacity, max_opacity) = *pulse_range;
1024                let placeholder = match *width_method {
1025                    "w_4_5" => div().w_3_4(),
1026                    "w_1_4" => div().w_1_4(),
1027                    "w_2_4" => div().w_2_4(),
1028                    "w_3_5" => div().w_3_5(),
1029                    "w_2_5" => div().w_2_5(),
1030                    _ => div().w_1_2(),
1031                }
1032                .id("loading_div")
1033                .h_1()
1034                .rounded_full()
1035                .bg(cx.theme().colors().element_active)
1036                .with_animation(
1037                    "loading_pulsate",
1038                    Animation::new(Duration::from_millis(*duration_ms))
1039                        .repeat()
1040                        .with_easing(pulsating_between(min_opacity, max_opacity)),
1041                    |label, delta| label.opacity(delta),
1042                );
1043
1044                container = container.child(placeholder);
1045            }
1046
1047            container
1048        };
1049
1050        v_flex()
1051            .mb_2()
1052            .border_1()
1053            .when(error_message.is_some(), |card| card.border_dashed())
1054            .border_color(border_color)
1055            .rounded_md()
1056            .overflow_hidden()
1057            .child(codeblock_header)
1058            .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
1059                card.child(
1060                    v_flex()
1061                        .p_2()
1062                        .gap_1()
1063                        .border_t_1()
1064                        .border_dashed()
1065                        .border_color(border_color)
1066                        .bg(cx.theme().colors().editor_background)
1067                        .rounded_b_md()
1068                        .child(
1069                            Label::new("Error")
1070                                .size(LabelSize::XSmall)
1071                                .color(Color::Error),
1072                        )
1073                        .child(
1074                            div()
1075                                .rounded_md()
1076                                .text_ui_sm(cx)
1077                                .bg(cx.theme().colors().editor_background)
1078                                .child(MarkdownElement::new(
1079                                    error_markdown.clone(),
1080                                    markdown_style(window, cx),
1081                                )),
1082                        ),
1083                )
1084            })
1085            .when(self.is_loading() && error_message.is_none(), |card| {
1086                card.child(waiting_for_diff)
1087            })
1088            .when(self.preview_expanded && !self.is_loading(), |card| {
1089                let editor_view = v_flex()
1090                    .relative()
1091                    .h_full()
1092                    .when(!self.full_height_expanded, |editor_container| {
1093                        editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0))
1094                    })
1095                    .overflow_hidden()
1096                    .border_t_1()
1097                    .border_color(border_color)
1098                    .bg(cx.theme().colors().editor_background)
1099                    .child(editor);
1100
1101                card.child(
1102                    ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id())
1103                        .with_total_lines(self.total_lines.unwrap_or(0) as usize)
1104                        .toggle_state(self.full_height_expanded)
1105                        .with_collapsed_fade()
1106                        .on_toggle({
1107                            let this = cx.entity().downgrade();
1108                            move |is_expanded, _window, cx| {
1109                                if let Some(this) = this.upgrade() {
1110                                    this.update(cx, |this, _cx| {
1111                                        this.full_height_expanded = is_expanded;
1112                                    });
1113                                }
1114                            }
1115                        }),
1116                )
1117            })
1118    }
1119}
1120
1121fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1122    let theme_settings = ThemeSettings::get_global(cx);
1123    let ui_font_size = TextSize::Default.rems(cx);
1124    let mut text_style = window.text_style();
1125
1126    text_style.refine(&TextStyleRefinement {
1127        font_family: Some(theme_settings.ui_font.family.clone()),
1128        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1129        font_features: Some(theme_settings.ui_font.features.clone()),
1130        font_size: Some(ui_font_size.into()),
1131        color: Some(cx.theme().colors().text),
1132        ..Default::default()
1133    });
1134
1135    MarkdownStyle {
1136        base_text_style: text_style.clone(),
1137        selection_background_color: cx.theme().colors().element_selection_background,
1138        ..Default::default()
1139    }
1140}
1141
1142async fn build_buffer(
1143    mut text: String,
1144    path: Arc<Path>,
1145    language_registry: &Arc<language::LanguageRegistry>,
1146    cx: &mut AsyncApp,
1147) -> Result<Entity<Buffer>> {
1148    let line_ending = LineEnding::detect(&text);
1149    LineEnding::normalize(&mut text);
1150    let text = Rope::from(text);
1151    let language = cx
1152        .update(|_cx| language_registry.language_for_file_path(&path))?
1153        .await
1154        .ok();
1155    let buffer = cx.new(|cx| {
1156        let buffer = TextBuffer::new_normalized(
1157            0,
1158            cx.entity_id().as_non_zero_u64().into(),
1159            line_ending,
1160            text,
1161        );
1162        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
1163        buffer.set_language(language, cx);
1164        buffer
1165    })?;
1166    Ok(buffer)
1167}
1168
1169async fn build_buffer_diff(
1170    old_text: Arc<String>,
1171    buffer: &Entity<Buffer>,
1172    language_registry: &Arc<LanguageRegistry>,
1173    cx: &mut AsyncApp,
1174) -> Result<Entity<BufferDiff>> {
1175    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
1176
1177    let old_text_rope = cx
1178        .background_spawn({
1179            let old_text = old_text.clone();
1180            async move { Rope::from(old_text.as_str()) }
1181        })
1182        .await;
1183    let base_buffer = cx
1184        .update(|cx| {
1185            Buffer::build_snapshot(
1186                old_text_rope,
1187                buffer.language().cloned(),
1188                Some(language_registry.clone()),
1189                cx,
1190            )
1191        })?
1192        .await;
1193
1194    let diff_snapshot = cx
1195        .update(|cx| {
1196            BufferDiffSnapshot::new_with_base_buffer(
1197                buffer.text.clone(),
1198                Some(old_text),
1199                base_buffer,
1200                cx,
1201            )
1202        })?
1203        .await;
1204
1205    let secondary_diff = cx.new(|cx| {
1206        let mut diff = BufferDiff::new(&buffer, cx);
1207        diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
1208        diff
1209    })?;
1210
1211    cx.new(|cx| {
1212        let mut diff = BufferDiff::new(&buffer.text, cx);
1213        diff.set_snapshot(diff_snapshot, &buffer, cx);
1214        diff.set_secondary_diff(secondary_diff);
1215        diff
1216    })
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use client::TelemetrySettings;
1223    use fs::{FakeFs, Fs};
1224    use gpui::{TestAppContext, UpdateGlobal};
1225    use language_model::fake_provider::FakeLanguageModel;
1226    use serde_json::json;
1227    use settings::SettingsStore;
1228    use util::path;
1229
1230    #[gpui::test]
1231    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
1232        init_test(cx);
1233
1234        let fs = FakeFs::new(cx.executor());
1235        fs.insert_tree("/root", json!({})).await;
1236        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1237        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1238        let model = Arc::new(FakeLanguageModel::default());
1239        let result = cx
1240            .update(|cx| {
1241                let input = serde_json::to_value(EditFileToolInput {
1242                    display_description: "Some edit".into(),
1243                    path: "root/nonexistent_file.txt".into(),
1244                    mode: EditFileMode::Edit,
1245                })
1246                .unwrap();
1247                Arc::new(EditFileTool)
1248                    .run(
1249                        input,
1250                        Arc::default(),
1251                        project.clone(),
1252                        action_log,
1253                        model,
1254                        None,
1255                        cx,
1256                    )
1257                    .output
1258            })
1259            .await;
1260        assert_eq!(
1261            result.unwrap_err().to_string(),
1262            "Can't edit file: path not found"
1263        );
1264    }
1265
1266    #[gpui::test]
1267    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
1268        let mode = &EditFileMode::Create;
1269
1270        let result = test_resolve_path(mode, "root/new.txt", cx);
1271        assert_resolved_path_eq(result.await, "new.txt");
1272
1273        let result = test_resolve_path(mode, "new.txt", cx);
1274        assert_resolved_path_eq(result.await, "new.txt");
1275
1276        let result = test_resolve_path(mode, "dir/new.txt", cx);
1277        assert_resolved_path_eq(result.await, "dir/new.txt");
1278
1279        let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
1280        assert_eq!(
1281            result.await.unwrap_err().to_string(),
1282            "Can't create file: file already exists"
1283        );
1284
1285        let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
1286        assert_eq!(
1287            result.await.unwrap_err().to_string(),
1288            "Can't create file: parent directory doesn't exist"
1289        );
1290    }
1291
1292    #[gpui::test]
1293    async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
1294        let mode = &EditFileMode::Edit;
1295
1296        let path_with_root = "root/dir/subdir/existing.txt";
1297        let path_without_root = "dir/subdir/existing.txt";
1298        let result = test_resolve_path(mode, path_with_root, cx);
1299        assert_resolved_path_eq(result.await, path_without_root);
1300
1301        let result = test_resolve_path(mode, path_without_root, cx);
1302        assert_resolved_path_eq(result.await, path_without_root);
1303
1304        let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
1305        assert_eq!(
1306            result.await.unwrap_err().to_string(),
1307            "Can't edit file: path not found"
1308        );
1309
1310        let result = test_resolve_path(mode, "root/dir", cx);
1311        assert_eq!(
1312            result.await.unwrap_err().to_string(),
1313            "Can't edit file: path is a directory"
1314        );
1315    }
1316
1317    async fn test_resolve_path(
1318        mode: &EditFileMode,
1319        path: &str,
1320        cx: &mut TestAppContext,
1321    ) -> anyhow::Result<ProjectPath> {
1322        init_test(cx);
1323
1324        let fs = FakeFs::new(cx.executor());
1325        fs.insert_tree(
1326            "/root",
1327            json!({
1328                "dir": {
1329                    "subdir": {
1330                        "existing.txt": "hello"
1331                    }
1332                }
1333            }),
1334        )
1335        .await;
1336        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1337
1338        let input = EditFileToolInput {
1339            display_description: "Some edit".into(),
1340            path: path.into(),
1341            mode: mode.clone(),
1342        };
1343
1344        let result = cx.update(|cx| resolve_path(&input, project, cx));
1345        result
1346    }
1347
1348    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
1349        let actual = path
1350            .expect("Should return valid path")
1351            .path
1352            .to_str()
1353            .unwrap()
1354            .replace("\\", "/"); // Naive Windows paths normalization
1355        assert_eq!(actual, expected);
1356    }
1357
1358    #[test]
1359    fn still_streaming_ui_text_with_path() {
1360        let input = json!({
1361            "path": "src/main.rs",
1362            "display_description": "",
1363            "old_string": "old code",
1364            "new_string": "new code"
1365        });
1366
1367        assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
1368    }
1369
1370    #[test]
1371    fn still_streaming_ui_text_with_description() {
1372        let input = json!({
1373            "path": "",
1374            "display_description": "Fix error handling",
1375            "old_string": "old code",
1376            "new_string": "new code"
1377        });
1378
1379        assert_eq!(
1380            EditFileTool.still_streaming_ui_text(&input),
1381            "Fix error handling",
1382        );
1383    }
1384
1385    #[test]
1386    fn still_streaming_ui_text_with_path_and_description() {
1387        let input = json!({
1388            "path": "src/main.rs",
1389            "display_description": "Fix error handling",
1390            "old_string": "old code",
1391            "new_string": "new code"
1392        });
1393
1394        assert_eq!(
1395            EditFileTool.still_streaming_ui_text(&input),
1396            "Fix error handling",
1397        );
1398    }
1399
1400    #[test]
1401    fn still_streaming_ui_text_no_path_or_description() {
1402        let input = json!({
1403            "path": "",
1404            "display_description": "",
1405            "old_string": "old code",
1406            "new_string": "new code"
1407        });
1408
1409        assert_eq!(
1410            EditFileTool.still_streaming_ui_text(&input),
1411            DEFAULT_UI_TEXT,
1412        );
1413    }
1414
1415    #[test]
1416    fn still_streaming_ui_text_with_null() {
1417        let input = serde_json::Value::Null;
1418
1419        assert_eq!(
1420            EditFileTool.still_streaming_ui_text(&input),
1421            DEFAULT_UI_TEXT,
1422        );
1423    }
1424
1425    fn init_test(cx: &mut TestAppContext) {
1426        cx.update(|cx| {
1427            let settings_store = SettingsStore::test(cx);
1428            cx.set_global(settings_store);
1429            language::init(cx);
1430            TelemetrySettings::register(cx);
1431            agent_settings::AgentSettings::register(cx);
1432            Project::init_settings(cx);
1433        });
1434    }
1435
1436    #[gpui::test]
1437    async fn test_format_on_save(cx: &mut TestAppContext) {
1438        init_test(cx);
1439
1440        let fs = FakeFs::new(cx.executor());
1441        fs.insert_tree("/root", json!({"src": {}})).await;
1442
1443        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1444
1445        // Set up a Rust language with LSP formatting support
1446        let rust_language = Arc::new(language::Language::new(
1447            language::LanguageConfig {
1448                name: "Rust".into(),
1449                matcher: language::LanguageMatcher {
1450                    path_suffixes: vec!["rs".to_string()],
1451                    ..Default::default()
1452                },
1453                ..Default::default()
1454            },
1455            None,
1456        ));
1457
1458        // Register the language and fake LSP
1459        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1460        language_registry.add(rust_language);
1461
1462        let mut fake_language_servers = language_registry.register_fake_lsp(
1463            "Rust",
1464            language::FakeLspAdapter {
1465                capabilities: lsp::ServerCapabilities {
1466                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
1467                    ..Default::default()
1468                },
1469                ..Default::default()
1470            },
1471        );
1472
1473        // Create the file
1474        fs.save(
1475            path!("/root/src/main.rs").as_ref(),
1476            &"initial content".into(),
1477            language::LineEnding::Unix,
1478        )
1479        .await
1480        .unwrap();
1481
1482        // Open the buffer to trigger LSP initialization
1483        let buffer = project
1484            .update(cx, |project, cx| {
1485                project.open_local_buffer(path!("/root/src/main.rs"), cx)
1486            })
1487            .await
1488            .unwrap();
1489
1490        // Register the buffer with language servers
1491        let _handle = project.update(cx, |project, cx| {
1492            project.register_buffer_with_language_servers(&buffer, cx)
1493        });
1494
1495        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
1496        const FORMATTED_CONTENT: &str =
1497            "This file was formatted by the fake formatter in the test.\n";
1498
1499        // Get the fake language server and set up formatting handler
1500        let fake_language_server = fake_language_servers.next().await.unwrap();
1501        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
1502            |_, _| async move {
1503                Ok(Some(vec![lsp::TextEdit {
1504                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
1505                    new_text: FORMATTED_CONTENT.to_string(),
1506                }]))
1507            }
1508        });
1509
1510        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1511        let model = Arc::new(FakeLanguageModel::default());
1512
1513        // First, test with format_on_save enabled
1514        cx.update(|cx| {
1515            SettingsStore::update_global(cx, |store, cx| {
1516                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1517                    cx,
1518                    |settings| {
1519                        settings.defaults.format_on_save = Some(FormatOnSave::On);
1520                        settings.defaults.formatter =
1521                            Some(language::language_settings::SelectedFormatter::Auto);
1522                    },
1523                );
1524            });
1525        });
1526
1527        // Have the model stream unformatted content
1528        let edit_result = {
1529            let edit_task = cx.update(|cx| {
1530                let input = serde_json::to_value(EditFileToolInput {
1531                    display_description: "Create main function".into(),
1532                    path: "root/src/main.rs".into(),
1533                    mode: EditFileMode::Overwrite,
1534                })
1535                .unwrap();
1536                Arc::new(EditFileTool)
1537                    .run(
1538                        input,
1539                        Arc::default(),
1540                        project.clone(),
1541                        action_log.clone(),
1542                        model.clone(),
1543                        None,
1544                        cx,
1545                    )
1546                    .output
1547            });
1548
1549            // Stream the unformatted content
1550            cx.executor().run_until_parked();
1551            model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1552            model.end_last_completion_stream();
1553
1554            edit_task.await
1555        };
1556        assert!(edit_result.is_ok());
1557
1558        // Wait for any async operations (e.g. formatting) to complete
1559        cx.executor().run_until_parked();
1560
1561        // Read the file to verify it was formatted automatically
1562        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1563        assert_eq!(
1564            // Ignore carriage returns on Windows
1565            new_content.replace("\r\n", "\n"),
1566            FORMATTED_CONTENT,
1567            "Code should be formatted when format_on_save is enabled"
1568        );
1569
1570        let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
1571
1572        assert_eq!(
1573            stale_buffer_count, 0,
1574            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
1575             This causes the agent to think the file was modified externally when it was just formatted.",
1576            stale_buffer_count
1577        );
1578
1579        // Next, test with format_on_save disabled
1580        cx.update(|cx| {
1581            SettingsStore::update_global(cx, |store, cx| {
1582                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1583                    cx,
1584                    |settings| {
1585                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
1586                    },
1587                );
1588            });
1589        });
1590
1591        // Stream unformatted edits again
1592        let edit_result = {
1593            let edit_task = cx.update(|cx| {
1594                let input = serde_json::to_value(EditFileToolInput {
1595                    display_description: "Update main function".into(),
1596                    path: "root/src/main.rs".into(),
1597                    mode: EditFileMode::Overwrite,
1598                })
1599                .unwrap();
1600                Arc::new(EditFileTool)
1601                    .run(
1602                        input,
1603                        Arc::default(),
1604                        project.clone(),
1605                        action_log.clone(),
1606                        model.clone(),
1607                        None,
1608                        cx,
1609                    )
1610                    .output
1611            });
1612
1613            // Stream the unformatted content
1614            cx.executor().run_until_parked();
1615            model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1616            model.end_last_completion_stream();
1617
1618            edit_task.await
1619        };
1620        assert!(edit_result.is_ok());
1621
1622        // Wait for any async operations (e.g. formatting) to complete
1623        cx.executor().run_until_parked();
1624
1625        // Verify the file was not formatted
1626        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1627        assert_eq!(
1628            // Ignore carriage returns on Windows
1629            new_content.replace("\r\n", "\n"),
1630            UNFORMATTED_CONTENT,
1631            "Code should not be formatted when format_on_save is disabled"
1632        );
1633    }
1634
1635    #[gpui::test]
1636    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
1637        init_test(cx);
1638
1639        let fs = FakeFs::new(cx.executor());
1640        fs.insert_tree("/root", json!({"src": {}})).await;
1641
1642        // Create a simple file with trailing whitespace
1643        fs.save(
1644            path!("/root/src/main.rs").as_ref(),
1645            &"initial content".into(),
1646            language::LineEnding::Unix,
1647        )
1648        .await
1649        .unwrap();
1650
1651        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1652        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1653        let model = Arc::new(FakeLanguageModel::default());
1654
1655        // First, test with remove_trailing_whitespace_on_save enabled
1656        cx.update(|cx| {
1657            SettingsStore::update_global(cx, |store, cx| {
1658                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1659                    cx,
1660                    |settings| {
1661                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
1662                    },
1663                );
1664            });
1665        });
1666
1667        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
1668            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
1669
1670        // Have the model stream content that contains trailing whitespace
1671        let edit_result = {
1672            let edit_task = cx.update(|cx| {
1673                let input = serde_json::to_value(EditFileToolInput {
1674                    display_description: "Create main function".into(),
1675                    path: "root/src/main.rs".into(),
1676                    mode: EditFileMode::Overwrite,
1677                })
1678                .unwrap();
1679                Arc::new(EditFileTool)
1680                    .run(
1681                        input,
1682                        Arc::default(),
1683                        project.clone(),
1684                        action_log.clone(),
1685                        model.clone(),
1686                        None,
1687                        cx,
1688                    )
1689                    .output
1690            });
1691
1692            // Stream the content with trailing whitespace
1693            cx.executor().run_until_parked();
1694            model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1695            model.end_last_completion_stream();
1696
1697            edit_task.await
1698        };
1699        assert!(edit_result.is_ok());
1700
1701        // Wait for any async operations (e.g. formatting) to complete
1702        cx.executor().run_until_parked();
1703
1704        // Read the file to verify trailing whitespace was removed automatically
1705        assert_eq!(
1706            // Ignore carriage returns on Windows
1707            fs.load(path!("/root/src/main.rs").as_ref())
1708                .await
1709                .unwrap()
1710                .replace("\r\n", "\n"),
1711            "fn main() {\n    println!(\"Hello!\");\n}\n",
1712            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
1713        );
1714
1715        // Next, test with remove_trailing_whitespace_on_save disabled
1716        cx.update(|cx| {
1717            SettingsStore::update_global(cx, |store, cx| {
1718                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1719                    cx,
1720                    |settings| {
1721                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
1722                    },
1723                );
1724            });
1725        });
1726
1727        // Stream edits again with trailing whitespace
1728        let edit_result = {
1729            let edit_task = cx.update(|cx| {
1730                let input = serde_json::to_value(EditFileToolInput {
1731                    display_description: "Update main function".into(),
1732                    path: "root/src/main.rs".into(),
1733                    mode: EditFileMode::Overwrite,
1734                })
1735                .unwrap();
1736                Arc::new(EditFileTool)
1737                    .run(
1738                        input,
1739                        Arc::default(),
1740                        project.clone(),
1741                        action_log.clone(),
1742                        model.clone(),
1743                        None,
1744                        cx,
1745                    )
1746                    .output
1747            });
1748
1749            // Stream the content with trailing whitespace
1750            cx.executor().run_until_parked();
1751            model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1752            model.end_last_completion_stream();
1753
1754            edit_task.await
1755        };
1756        assert!(edit_result.is_ok());
1757
1758        // Wait for any async operations (e.g. formatting) to complete
1759        cx.executor().run_until_parked();
1760
1761        // Verify the file still has trailing whitespace
1762        // Read the file again - it should still have trailing whitespace
1763        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1764        assert_eq!(
1765            // Ignore carriage returns on Windows
1766            final_content.replace("\r\n", "\n"),
1767            CONTENT_WITH_TRAILING_WHITESPACE,
1768            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
1769        );
1770    }
1771
1772    #[gpui::test]
1773    async fn test_needs_confirmation(cx: &mut TestAppContext) {
1774        init_test(cx);
1775        let tool = Arc::new(EditFileTool);
1776        let fs = FakeFs::new(cx.executor());
1777        fs.insert_tree("/root", json!({})).await;
1778
1779        // Test 1: Path with .zed component should require confirmation
1780        let input_with_zed = json!({
1781            "display_description": "Edit settings",
1782            "path": ".zed/settings.json",
1783            "mode": "edit"
1784        });
1785        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1786        cx.update(|cx| {
1787            assert!(
1788                tool.needs_confirmation(&input_with_zed, &project, cx),
1789                "Path with .zed component should require confirmation"
1790            );
1791        });
1792
1793        // Test 2: Absolute path should require confirmation
1794        let input_absolute = json!({
1795            "display_description": "Edit file",
1796            "path": "/etc/hosts",
1797            "mode": "edit"
1798        });
1799        cx.update(|cx| {
1800            assert!(
1801                tool.needs_confirmation(&input_absolute, &project, cx),
1802                "Absolute path should require confirmation"
1803            );
1804        });
1805
1806        // Test 3: Relative path without .zed should not require confirmation
1807        let input_relative = json!({
1808            "display_description": "Edit file",
1809            "path": "root/src/main.rs",
1810            "mode": "edit"
1811        });
1812        cx.update(|cx| {
1813            assert!(
1814                !tool.needs_confirmation(&input_relative, &project, cx),
1815                "Relative path without .zed should not require confirmation"
1816            );
1817        });
1818
1819        // Test 4: Path with .zed in the middle should require confirmation
1820        let input_zed_middle = json!({
1821            "display_description": "Edit settings",
1822            "path": "root/.zed/tasks.json",
1823            "mode": "edit"
1824        });
1825        cx.update(|cx| {
1826            assert!(
1827                tool.needs_confirmation(&input_zed_middle, &project, cx),
1828                "Path with .zed in any component should require confirmation"
1829            );
1830        });
1831
1832        // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
1833        cx.update(|cx| {
1834            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1835            settings.always_allow_tool_actions = true;
1836            agent_settings::AgentSettings::override_global(settings, cx);
1837
1838            assert!(
1839                !tool.needs_confirmation(&input_with_zed, &project, cx),
1840                "When always_allow_tool_actions is true, no confirmation should be needed"
1841            );
1842            assert!(
1843                !tool.needs_confirmation(&input_absolute, &project, cx),
1844                "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
1845            );
1846        });
1847    }
1848
1849    #[gpui::test]
1850    fn test_ui_text_with_confirmation_context(cx: &mut TestAppContext) {
1851        init_test(cx);
1852        let tool = Arc::new(EditFileTool);
1853
1854        // Test ui_text shows context for .zed paths
1855        let input_zed = json!({
1856            "display_description": "Update settings",
1857            "path": ".zed/settings.json",
1858            "mode": "edit"
1859        });
1860        cx.update(|_cx| {
1861            let ui_text = tool.ui_text(&input_zed);
1862            assert_eq!(
1863                ui_text, "Update settings (Zed settings)",
1864                "UI text should indicate Zed settings file"
1865            );
1866        });
1867
1868        // Test ui_text for normal paths
1869        let input_normal = json!({
1870            "display_description": "Edit source file",
1871            "path": "src/main.rs",
1872            "mode": "edit"
1873        });
1874        cx.update(|_cx| {
1875            let ui_text = tool.ui_text(&input_normal);
1876            assert_eq!(
1877                ui_text, "Edit source file",
1878                "UI text should not have additional context for normal paths"
1879            );
1880        });
1881    }
1882
1883    #[gpui::test]
1884    async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
1885        init_test(cx);
1886        let tool = Arc::new(EditFileTool);
1887        let fs = FakeFs::new(cx.executor());
1888
1889        // Create a project in /project directory
1890        fs.insert_tree("/project", json!({})).await;
1891        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1892
1893        // Test file outside project requires confirmation
1894        let input_outside = json!({
1895            "display_description": "Edit file",
1896            "path": "/outside/file.txt",
1897            "mode": "edit"
1898        });
1899        cx.update(|cx| {
1900            assert!(
1901                tool.needs_confirmation(&input_outside, &project, cx),
1902                "File outside project should require confirmation"
1903            );
1904        });
1905
1906        // Test file inside project doesn't require confirmation
1907        let input_inside = json!({
1908            "display_description": "Edit file",
1909            "path": "project/file.txt",
1910            "mode": "edit"
1911        });
1912        cx.update(|cx| {
1913            assert!(
1914                !tool.needs_confirmation(&input_inside, &project, cx),
1915                "File inside project should not require confirmation"
1916            );
1917        });
1918    }
1919
1920    #[gpui::test]
1921    async fn test_needs_confirmation_zed_paths(cx: &mut TestAppContext) {
1922        init_test(cx);
1923        let tool = Arc::new(EditFileTool);
1924        let fs = FakeFs::new(cx.executor());
1925        fs.insert_tree("/home/user/myproject", json!({})).await;
1926        let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
1927
1928        // Test various .zed path patterns
1929        let test_cases = vec![
1930            (".zed/settings.json", true, "Top-level .zed file"),
1931            ("myproject/.zed/settings.json", true, ".zed in project path"),
1932            ("src/.zed/config.toml", true, ".zed in subdirectory"),
1933            (
1934                ".zed.backup/file.txt",
1935                true,
1936                ".zed.backup is outside project (not a .zed component issue)",
1937            ),
1938            (
1939                "my.zed/file.txt",
1940                true,
1941                "my.zed is outside project (not a .zed component issue)",
1942            ),
1943            ("myproject/src/file.zed", false, ".zed as file extension"),
1944            (
1945                "myproject/normal/path/file.rs",
1946                false,
1947                "Normal file without .zed",
1948            ),
1949        ];
1950
1951        for (path, should_confirm, description) in test_cases {
1952            let input = json!({
1953                "display_description": "Edit file",
1954                "path": path,
1955                "mode": "edit"
1956            });
1957            cx.update(|cx| {
1958                assert_eq!(
1959                    tool.needs_confirmation(&input, &project, cx),
1960                    should_confirm,
1961                    "Failed for case: {} - path: {}",
1962                    description,
1963                    path
1964                );
1965            });
1966        }
1967    }
1968
1969    #[gpui::test]
1970    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
1971        init_test(cx);
1972        let tool = Arc::new(EditFileTool);
1973        let fs = FakeFs::new(cx.executor());
1974
1975        // Create multiple worktree directories
1976        fs.insert_tree(
1977            "/workspace/frontend",
1978            json!({
1979                "src": {
1980                    "main.js": "console.log('frontend');"
1981                }
1982            }),
1983        )
1984        .await;
1985        fs.insert_tree(
1986            "/workspace/backend",
1987            json!({
1988                "src": {
1989                    "main.rs": "fn main() {}"
1990                }
1991            }),
1992        )
1993        .await;
1994        fs.insert_tree(
1995            "/workspace/shared",
1996            json!({
1997                ".zed": {
1998                    "settings.json": "{}"
1999                }
2000            }),
2001        )
2002        .await;
2003
2004        // Create project with multiple worktrees
2005        let project = Project::test(
2006            fs.clone(),
2007            [
2008                path!("/workspace/frontend").as_ref(),
2009                path!("/workspace/backend").as_ref(),
2010                path!("/workspace/shared").as_ref(),
2011            ],
2012            cx,
2013        )
2014        .await;
2015
2016        // Test files in different worktrees
2017        let test_cases = vec![
2018            ("frontend/src/main.js", false, "File in first worktree"),
2019            ("backend/src/main.rs", false, "File in second worktree"),
2020            (
2021                "shared/.zed/settings.json",
2022                true,
2023                ".zed file in third worktree",
2024            ),
2025            ("/etc/hosts", true, "Absolute path outside all worktrees"),
2026            (
2027                "../outside/file.txt",
2028                true,
2029                "Relative path outside worktrees",
2030            ),
2031        ];
2032
2033        for (path, should_confirm, description) in test_cases {
2034            let input = json!({
2035                "display_description": "Edit file",
2036                "path": path,
2037                "mode": "edit"
2038            });
2039            cx.update(|cx| {
2040                assert_eq!(
2041                    tool.needs_confirmation(&input, &project, cx),
2042                    should_confirm,
2043                    "Failed for case: {} - path: {}",
2044                    description,
2045                    path
2046                );
2047            });
2048        }
2049    }
2050
2051    #[gpui::test]
2052    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
2053        init_test(cx);
2054        let tool = Arc::new(EditFileTool);
2055        let fs = FakeFs::new(cx.executor());
2056        fs.insert_tree(
2057            "/project",
2058            json!({
2059                ".zed": {
2060                    "settings.json": "{}"
2061                },
2062                "src": {
2063                    ".zed": {
2064                        "local.json": "{}"
2065                    }
2066                }
2067            }),
2068        )
2069        .await;
2070        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2071
2072        // Test edge cases
2073        let test_cases = vec![
2074            // Empty path - find_project_path returns Some for empty paths
2075            ("", false, "Empty path is treated as project root"),
2076            // Root directory
2077            ("/", true, "Root directory should be outside project"),
2078            // Parent directory references - find_project_path resolves these
2079            (
2080                "project/../other",
2081                false,
2082                "Path with .. is resolved by find_project_path",
2083            ),
2084            (
2085                "project/./src/file.rs",
2086                false,
2087                "Path with . should work normally",
2088            ),
2089            // Windows-style paths (if on Windows)
2090            #[cfg(target_os = "windows")]
2091            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
2092            #[cfg(target_os = "windows")]
2093            ("project\\src\\main.rs", false, "Windows-style project path"),
2094        ];
2095
2096        for (path, should_confirm, description) in test_cases {
2097            let input = json!({
2098                "display_description": "Edit file",
2099                "path": path,
2100                "mode": "edit"
2101            });
2102            cx.update(|cx| {
2103                assert_eq!(
2104                    tool.needs_confirmation(&input, &project, cx),
2105                    should_confirm,
2106                    "Failed for case: {} - path: {}",
2107                    description,
2108                    path
2109                );
2110            });
2111        }
2112    }
2113
2114    #[gpui::test]
2115    async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
2116        init_test(cx);
2117        let tool = Arc::new(EditFileTool);
2118
2119        // Test UI text for various scenarios
2120        let test_cases = vec![
2121            (
2122                json!({
2123                    "display_description": "Update config",
2124                    "path": ".zed/settings.json",
2125                    "mode": "edit"
2126                }),
2127                "Update config (Zed settings)",
2128                ".zed path should show Zed settings context",
2129            ),
2130            (
2131                json!({
2132                    "display_description": "Fix bug",
2133                    "path": "src/.zed/local.json",
2134                    "mode": "edit"
2135                }),
2136                "Fix bug (Zed settings)",
2137                "Nested .zed path should show Zed settings context",
2138            ),
2139            (
2140                json!({
2141                    "display_description": "Update readme",
2142                    "path": "README.md",
2143                    "mode": "edit"
2144                }),
2145                "Update readme",
2146                "Normal path should not show additional context",
2147            ),
2148            (
2149                json!({
2150                    "display_description": "Edit config",
2151                    "path": "config.zed",
2152                    "mode": "edit"
2153                }),
2154                "Edit config",
2155                ".zed as extension should not show context",
2156            ),
2157        ];
2158
2159        for (input, expected_text, description) in test_cases {
2160            cx.update(|_cx| {
2161                let ui_text = tool.ui_text(&input);
2162                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
2163            });
2164        }
2165    }
2166
2167    #[gpui::test]
2168    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
2169        init_test(cx);
2170        let tool = Arc::new(EditFileTool);
2171        let fs = FakeFs::new(cx.executor());
2172        fs.insert_tree(
2173            "/project",
2174            json!({
2175                "existing.txt": "content",
2176                ".zed": {
2177                    "settings.json": "{}"
2178                }
2179            }),
2180        )
2181        .await;
2182        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2183
2184        // Test different EditFileMode values
2185        let modes = vec![
2186            EditFileMode::Edit,
2187            EditFileMode::Create,
2188            EditFileMode::Overwrite,
2189        ];
2190
2191        for mode in modes {
2192            // Test .zed path with different modes
2193            let input_zed = json!({
2194                "display_description": "Edit settings",
2195                "path": "project/.zed/settings.json",
2196                "mode": mode
2197            });
2198            cx.update(|cx| {
2199                assert!(
2200                    tool.needs_confirmation(&input_zed, &project, cx),
2201                    ".zed path should require confirmation regardless of mode: {:?}",
2202                    mode
2203                );
2204            });
2205
2206            // Test outside path with different modes
2207            let input_outside = json!({
2208                "display_description": "Edit file",
2209                "path": "/outside/file.txt",
2210                "mode": mode
2211            });
2212            cx.update(|cx| {
2213                assert!(
2214                    tool.needs_confirmation(&input_outside, &project, cx),
2215                    "Outside path should require confirmation regardless of mode: {:?}",
2216                    mode
2217                );
2218            });
2219
2220            // Test normal path with different modes
2221            let input_normal = json!({
2222                "display_description": "Edit file",
2223                "path": "project/normal.txt",
2224                "mode": mode
2225            });
2226            cx.update(|cx| {
2227                assert!(
2228                    !tool.needs_confirmation(&input_normal, &project, cx),
2229                    "Normal path should not require confirmation regardless of mode: {:?}",
2230                    mode
2231                );
2232            });
2233        }
2234    }
2235
2236    #[gpui::test]
2237    async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
2238        init_test(cx);
2239        let tool = Arc::new(EditFileTool);
2240        let fs = FakeFs::new(cx.executor());
2241        fs.insert_tree("/project", json!({})).await;
2242        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2243
2244        // Enable always_allow_tool_actions
2245        cx.update(|cx| {
2246            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
2247            settings.always_allow_tool_actions = true;
2248            agent_settings::AgentSettings::override_global(settings, cx);
2249        });
2250
2251        // Test that all paths that normally require confirmation are bypassed
2252        let test_cases = vec![
2253            ".zed/settings.json",
2254            "project/.zed/config.toml",
2255            "/etc/hosts",
2256            "/absolute/path/file.txt",
2257            "../outside/project.txt",
2258        ];
2259
2260        for path in test_cases {
2261            let input = json!({
2262                "display_description": "Edit file",
2263                "path": path,
2264                "mode": "edit"
2265            });
2266            cx.update(|cx| {
2267                assert!(
2268                    !tool.needs_confirmation(&input, &project, cx),
2269                    "Path {} should not require confirmation when always_allow_tool_actions is true",
2270                    path
2271                );
2272            });
2273        }
2274
2275        // Disable always_allow_tool_actions and verify confirmation is required again
2276        cx.update(|cx| {
2277            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
2278            settings.always_allow_tool_actions = false;
2279            agent_settings::AgentSettings::override_global(settings, cx);
2280        });
2281
2282        // Verify .zed path requires confirmation again
2283        let input = json!({
2284            "display_description": "Edit file",
2285            "path": ".zed/settings.json",
2286            "mode": "edit"
2287        });
2288        cx.update(|cx| {
2289            assert!(
2290                tool.needs_confirmation(&input, &project, cx),
2291                ".zed path should require confirmation when always_allow_tool_actions is false"
2292            );
2293        });
2294    }
2295}