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