edit_file_tool.rs

   1use crate::{
   2    Templates,
   3    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
   4    schema::json_schema_for,
   5    ui::{COLLAPSED_LINES, ToolOutputPreview},
   6};
   7use anyhow::{Context as _, Result, anyhow};
   8use assistant_tool::{
   9    ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
  10    ToolUseStatus,
  11};
  12use buffer_diff::{BufferDiff, BufferDiffSnapshot};
  13use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
  14use futures::StreamExt;
  15use gpui::{
  16    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
  17    TextStyleRefinement, WeakEntity, pulsating_between, px,
  18};
  19use indoc::formatdoc;
  20use language::{
  21    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
  22    TextBuffer,
  23    language_settings::{self, FormatOnSave, SoftWrap},
  24};
  25use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
  26use markdown::{Markdown, MarkdownElement, MarkdownStyle};
  27use project::{
  28    Project, ProjectPath,
  29    lsp_store::{FormatTrigger, LspFormatTarget},
  30};
  31use schemars::JsonSchema;
  32use serde::{Deserialize, Serialize};
  33use settings::Settings;
  34use std::{
  35    cmp::Reverse,
  36    collections::HashSet,
  37    ops::Range,
  38    path::{Path, PathBuf},
  39    sync::Arc,
  40    time::Duration,
  41};
  42use theme::ThemeSettings;
  43use ui::{Disclosure, Tooltip, prelude::*};
  44use util::ResultExt;
  45use workspace::Workspace;
  46
  47pub struct EditFileTool;
  48
  49#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  50pub struct EditFileToolInput {
  51    /// A one-line, user-friendly markdown description of the edit. This will be
  52    /// shown in the UI and also passed to another model to perform the edit.
  53    ///
  54    /// Be terse, but also descriptive in what you want to achieve with this
  55    /// edit. Avoid generic instructions.
  56    ///
  57    /// NEVER mention the file path in this description.
  58    ///
  59    /// <example>Fix API endpoint URLs</example>
  60    /// <example>Update copyright year in `page_footer`</example>
  61    ///
  62    /// Make sure to include this field before all the others in the input object
  63    /// so that we can display it immediately.
  64    pub display_description: String,
  65
  66    /// The full path of the file to create or modify in the project.
  67    ///
  68    /// WARNING: When specifying which file path need changing, you MUST
  69    /// start each path with one of the project's root directories.
  70    ///
  71    /// The following examples assume we have two root directories in the project:
  72    /// - backend
  73    /// - frontend
  74    ///
  75    /// <example>
  76    /// `backend/src/main.rs`
  77    ///
  78    /// Notice how the file path starts with root-1. Without that, the path
  79    /// would be ambiguous and the call would fail!
  80    /// </example>
  81    ///
  82    /// <example>
  83    /// `frontend/db.js`
  84    /// </example>
  85    pub path: PathBuf,
  86
  87    /// The mode of operation on the file. Possible values:
  88    /// - 'edit': Make granular edits to an existing file.
  89    /// - 'create': Create a new file if it doesn't exist.
  90    /// - 'overwrite': Replace the entire contents of an existing file.
  91    ///
  92    /// When a file already exists or you just created it, prefer editing
  93    /// it as opposed to recreating it from scratch.
  94    pub mode: EditFileMode,
  95}
  96
  97#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  98#[serde(rename_all = "lowercase")]
  99pub enum EditFileMode {
 100    Edit,
 101    Create,
 102    Overwrite,
 103}
 104
 105#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 106pub struct EditFileToolOutput {
 107    pub original_path: PathBuf,
 108    pub new_text: String,
 109    pub old_text: Arc<String>,
 110    pub raw_output: Option<EditAgentOutput>,
 111}
 112
 113#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 114struct PartialInput {
 115    #[serde(default)]
 116    path: String,
 117    #[serde(default)]
 118    display_description: String,
 119}
 120
 121const DEFAULT_UI_TEXT: &str = "Editing file";
 122
 123impl Tool for EditFileTool {
 124    fn name(&self) -> String {
 125        "edit_file".into()
 126    }
 127
 128    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
 129        false
 130    }
 131
 132    fn may_perform_edits(&self) -> bool {
 133        true
 134    }
 135
 136    fn description(&self) -> String {
 137        include_str!("edit_file_tool/description.md").to_string()
 138    }
 139
 140    fn icon(&self) -> IconName {
 141        IconName::Pencil
 142    }
 143
 144    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
 145        json_schema_for::<EditFileToolInput>(format)
 146    }
 147
 148    fn ui_text(&self, input: &serde_json::Value) -> String {
 149        match serde_json::from_value::<EditFileToolInput>(input.clone()) {
 150            Ok(input) => input.display_description,
 151            Err(_) => "Editing file".to_string(),
 152        }
 153    }
 154
 155    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
 156        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
 157            let description = input.display_description.trim();
 158            if !description.is_empty() {
 159                return description.to_string();
 160            }
 161
 162            let path = input.path.trim();
 163            if !path.is_empty() {
 164                return path.to_string();
 165            }
 166        }
 167
 168        DEFAULT_UI_TEXT.to_string()
 169    }
 170
 171    fn run(
 172        self: Arc<Self>,
 173        input: serde_json::Value,
 174        request: Arc<LanguageModelRequest>,
 175        project: Entity<Project>,
 176        action_log: Entity<ActionLog>,
 177        model: Arc<dyn LanguageModel>,
 178        window: Option<AnyWindowHandle>,
 179        cx: &mut App,
 180    ) -> ToolResult {
 181        let input = match serde_json::from_value::<EditFileToolInput>(input) {
 182            Ok(input) => input,
 183            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
 184        };
 185
 186        let project_path = match resolve_path(&input, project.clone(), cx) {
 187            Ok(path) => path,
 188            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
 189        };
 190
 191        let card = window.and_then(|window| {
 192            window
 193                .update(cx, |_, window, cx| {
 194                    cx.new(|cx| {
 195                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
 196                    })
 197                })
 198                .ok()
 199        });
 200
 201        let card_clone = card.clone();
 202        let action_log_clone = action_log.clone();
 203        let task = cx.spawn(async move |cx: &mut AsyncApp| {
 204            let edit_agent =
 205                EditAgent::new(model, project.clone(), action_log_clone, Templates::new());
 206
 207            let buffer = project
 208                .update(cx, |project, cx| {
 209                    project.open_buffer(project_path.clone(), cx)
 210                })?
 211                .await?;
 212
 213            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 214            let old_text = cx
 215                .background_spawn({
 216                    let old_snapshot = old_snapshot.clone();
 217                    async move { Arc::new(old_snapshot.text()) }
 218                })
 219                .await;
 220
 221            if let Some(card) = card_clone.as_ref() {
 222                card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;
 223            }
 224
 225            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
 226                edit_agent.edit(
 227                    buffer.clone(),
 228                    input.display_description.clone(),
 229                    &request,
 230                    cx,
 231                )
 232            } else {
 233                edit_agent.overwrite(
 234                    buffer.clone(),
 235                    input.display_description.clone(),
 236                    &request,
 237                    cx,
 238                )
 239            };
 240
 241            let mut hallucinated_old_text = false;
 242            let mut ambiguous_ranges = Vec::new();
 243            while let Some(event) = events.next().await {
 244                match event {
 245                    EditAgentOutputEvent::Edited => {
 246                        if let Some(card) = card_clone.as_ref() {
 247                            card.update(cx, |card, cx| card.update_diff(cx))?;
 248                        }
 249                    }
 250                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
 251                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
 252                    EditAgentOutputEvent::ResolvingEditRange(range) => {
 253                        if let Some(card) = card_clone.as_ref() {
 254                            card.update(cx, |card, cx| card.reveal_range(range, cx))?;
 255                        }
 256                    }
 257                }
 258            }
 259            let agent_output = output.await?;
 260
 261            // If format_on_save is enabled, format the buffer
 262            let format_on_save_enabled = buffer
 263                .read_with(cx, |buffer, cx| {
 264                    let settings = language_settings::language_settings(
 265                        buffer.language().map(|l| l.name()),
 266                        buffer.file(),
 267                        cx,
 268                    );
 269                    !matches!(settings.format_on_save, FormatOnSave::Off)
 270                })
 271                .unwrap_or(false);
 272
 273            if format_on_save_enabled {
 274                let format_task = project.update(cx, |project, cx| {
 275                    project.format(
 276                        HashSet::from_iter([buffer.clone()]),
 277                        LspFormatTarget::Buffers,
 278                        false, // Don't push to history since the tool did it.
 279                        FormatTrigger::Save,
 280                        cx,
 281                    )
 282                })?;
 283                format_task.await.log_err();
 284            }
 285
 286            project
 287                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
 288                .await?;
 289
 290            // Notify the action log that we've edited the buffer (*after* formatting has completed).
 291            action_log.update(cx, |log, cx| {
 292                log.buffer_edited(buffer.clone(), cx);
 293            })?;
 294
 295            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 296            let (new_text, diff) = cx
 297                .background_spawn({
 298                    let new_snapshot = new_snapshot.clone();
 299                    let old_text = old_text.clone();
 300                    async move {
 301                        let new_text = new_snapshot.text();
 302                        let diff = language::unified_diff(&old_text, &new_text);
 303
 304                        (new_text, diff)
 305                    }
 306                })
 307                .await;
 308
 309            let output = EditFileToolOutput {
 310                original_path: project_path.path.to_path_buf(),
 311                new_text: new_text.clone(),
 312                old_text,
 313                raw_output: Some(agent_output),
 314            };
 315
 316            if let Some(card) = card_clone {
 317                card.update(cx, |card, cx| {
 318                    card.update_diff(cx);
 319                    card.finalize(cx)
 320                })
 321                .log_err();
 322            }
 323
 324            let input_path = input.path.display();
 325            if diff.is_empty() {
 326                anyhow::ensure!(
 327                    !hallucinated_old_text,
 328                    formatdoc! {"
 329                        Some edits were produced but none of them could be applied.
 330                        Read the relevant sections of {input_path} again so that
 331                        I can perform the requested edits.
 332                    "}
 333                );
 334                anyhow::ensure!(
 335                    ambiguous_ranges.is_empty(),
 336                    // TODO: Include ambiguous_ranges, converted to line numbers.
 337                    //       This would work best if we add `line_hint` parameter
 338                    //       to edit_file_tool
 339                    formatdoc! {"
 340                        <old_text> matches more than one position in the file. Read the
 341                        relevant sections of {input_path} again and extend <old_text> so
 342                        that I can perform the requested edits.
 343                    "}
 344                );
 345                Ok(ToolResultOutput {
 346                    content: ToolResultContent::Text("No edits were made.".into()),
 347                    output: serde_json::to_value(output).ok(),
 348                })
 349            } else {
 350                Ok(ToolResultOutput {
 351                    content: ToolResultContent::Text(format!(
 352                        "Edited {}:\n\n```diff\n{}\n```",
 353                        input_path, diff
 354                    )),
 355                    output: serde_json::to_value(output).ok(),
 356                })
 357            }
 358        });
 359
 360        ToolResult {
 361            output: task,
 362            card: card.map(AnyToolCard::from),
 363        }
 364    }
 365
 366    fn deserialize_card(
 367        self: Arc<Self>,
 368        output: serde_json::Value,
 369        project: Entity<Project>,
 370        window: &mut Window,
 371        cx: &mut App,
 372    ) -> Option<AnyToolCard> {
 373        let output = match serde_json::from_value::<EditFileToolOutput>(output) {
 374            Ok(output) => output,
 375            Err(_) => return None,
 376        };
 377
 378        let card = cx.new(|cx| {
 379            EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)
 380        });
 381
 382        cx.spawn({
 383            let path: Arc<Path> = output.original_path.into();
 384            let language_registry = project.read(cx).languages().clone();
 385            let card = card.clone();
 386            async move |cx| {
 387                let buffer =
 388                    build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;
 389                let buffer_diff =
 390                    build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)
 391                        .await?;
 392                card.update(cx, |card, cx| {
 393                    card.multibuffer.update(cx, |multibuffer, cx| {
 394                        let snapshot = buffer.read(cx).snapshot();
 395                        let diff = buffer_diff.read(cx);
 396                        let diff_hunk_ranges = diff
 397                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
 398                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
 399                            .collect::<Vec<_>>();
 400
 401                        multibuffer.set_excerpts_for_path(
 402                            PathKey::for_buffer(&buffer, cx),
 403                            buffer,
 404                            diff_hunk_ranges,
 405                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
 406                            cx,
 407                        );
 408                        multibuffer.add_diff(buffer_diff, cx);
 409                        let end = multibuffer.len(cx);
 410                        card.total_lines =
 411                            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);
 412                    });
 413
 414                    cx.notify();
 415                })?;
 416                anyhow::Ok(())
 417            }
 418        })
 419        .detach_and_log_err(cx);
 420
 421        Some(card.into())
 422    }
 423}
 424
 425/// Validate that the file path is valid, meaning:
 426///
 427/// - For `edit` and `overwrite`, the path must point to an existing file.
 428/// - For `create`, the file must not already exist, but it's parent dir must exist.
 429fn resolve_path(
 430    input: &EditFileToolInput,
 431    project: Entity<Project>,
 432    cx: &mut App,
 433) -> Result<ProjectPath> {
 434    let project = project.read(cx);
 435
 436    match input.mode {
 437        EditFileMode::Edit | EditFileMode::Overwrite => {
 438            let path = project
 439                .find_project_path(&input.path, cx)
 440                .context("Can't edit file: path not found")?;
 441
 442            let entry = project
 443                .entry_for_path(&path, cx)
 444                .context("Can't edit file: path not found")?;
 445
 446            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
 447            Ok(path)
 448        }
 449
 450        EditFileMode::Create => {
 451            if let Some(path) = project.find_project_path(&input.path, cx) {
 452                anyhow::ensure!(
 453                    project.entry_for_path(&path, cx).is_none(),
 454                    "Can't create file: file already exists"
 455                );
 456            }
 457
 458            let parent_path = input
 459                .path
 460                .parent()
 461                .context("Can't create file: incorrect path")?;
 462
 463            let parent_project_path = project.find_project_path(&parent_path, cx);
 464
 465            let parent_entry = parent_project_path
 466                .as_ref()
 467                .and_then(|path| project.entry_for_path(&path, cx))
 468                .context("Can't create file: parent directory doesn't exist")?;
 469
 470            anyhow::ensure!(
 471                parent_entry.is_dir(),
 472                "Can't create file: parent is not a directory"
 473            );
 474
 475            let file_name = input
 476                .path
 477                .file_name()
 478                .context("Can't create file: invalid filename")?;
 479
 480            let new_file_path = parent_project_path.map(|parent| ProjectPath {
 481                path: Arc::from(parent.path.join(file_name)),
 482                ..parent
 483            });
 484
 485            new_file_path.context("Can't create file")
 486        }
 487    }
 488}
 489
 490pub struct EditFileToolCard {
 491    path: PathBuf,
 492    editor: Entity<Editor>,
 493    multibuffer: Entity<MultiBuffer>,
 494    project: Entity<Project>,
 495    buffer: Option<Entity<Buffer>>,
 496    base_text: Option<Arc<String>>,
 497    buffer_diff: Option<Entity<BufferDiff>>,
 498    revealed_ranges: Vec<Range<Anchor>>,
 499    diff_task: Option<Task<Result<()>>>,
 500    preview_expanded: bool,
 501    error_expanded: Option<Entity<Markdown>>,
 502    full_height_expanded: bool,
 503    total_lines: Option<u32>,
 504}
 505
 506impl EditFileToolCard {
 507    pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
 508        let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
 509        let editor = cx.new(|cx| {
 510            let mut editor = Editor::new(
 511                EditorMode::Full {
 512                    scale_ui_elements_with_buffer_font_size: false,
 513                    show_active_line_background: false,
 514                    sized_by_content: true,
 515                },
 516                multibuffer.clone(),
 517                Some(project.clone()),
 518                window,
 519                cx,
 520            );
 521            editor.set_show_gutter(false, cx);
 522            editor.disable_inline_diagnostics();
 523            editor.disable_expand_excerpt_buttons(cx);
 524            // Keep horizontal scrollbar so user can scroll horizontally if needed
 525            editor.set_show_vertical_scrollbar(false, cx);
 526            editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 527            editor.set_soft_wrap_mode(SoftWrap::None, cx);
 528            editor.scroll_manager.set_forbid_vertical_scroll(true);
 529            editor.set_show_indent_guides(false, cx);
 530            editor.set_read_only(true);
 531            editor.set_show_breakpoints(false, cx);
 532            editor.set_show_code_actions(false, cx);
 533            editor.set_show_git_diff_gutter(false, cx);
 534            editor.set_expand_all_diff_hunks(cx);
 535            editor
 536        });
 537        Self {
 538            path,
 539            project,
 540            editor,
 541            multibuffer,
 542            buffer: None,
 543            base_text: None,
 544            buffer_diff: None,
 545            revealed_ranges: Vec::new(),
 546            diff_task: None,
 547            preview_expanded: true,
 548            error_expanded: None,
 549            full_height_expanded: true,
 550            total_lines: None,
 551        }
 552    }
 553
 554    pub fn initialize(&mut self, buffer: Entity<Buffer>, cx: &mut App) {
 555        let buffer_snapshot = buffer.read(cx).snapshot();
 556        let base_text = buffer_snapshot.text();
 557        let language_registry = buffer.read(cx).language_registry();
 558        let text_snapshot = buffer.read(cx).text_snapshot();
 559
 560        // Create a buffer diff with the current text as the base
 561        let buffer_diff = cx.new(|cx| {
 562            let mut diff = BufferDiff::new(&text_snapshot, cx);
 563            let _ = diff.set_base_text(
 564                buffer_snapshot.clone(),
 565                language_registry,
 566                text_snapshot,
 567                cx,
 568            );
 569            diff
 570        });
 571
 572        self.buffer = Some(buffer.clone());
 573        self.base_text = Some(base_text.into());
 574        self.buffer_diff = Some(buffer_diff.clone());
 575
 576        // Add the diff to the multibuffer
 577        self.multibuffer
 578            .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx));
 579    }
 580
 581    pub fn is_loading(&self) -> bool {
 582        self.total_lines.is_none()
 583    }
 584
 585    pub fn update_diff(&mut self, cx: &mut Context<Self>) {
 586        let Some(buffer) = self.buffer.as_ref() else {
 587            return;
 588        };
 589        let Some(buffer_diff) = self.buffer_diff.as_ref() else {
 590            return;
 591        };
 592
 593        let buffer = buffer.clone();
 594        let buffer_diff = buffer_diff.clone();
 595        let base_text = self.base_text.clone();
 596        self.diff_task = Some(cx.spawn(async move |this, cx| {
 597            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
 598            let diff_snapshot = BufferDiff::update_diff(
 599                buffer_diff.clone(),
 600                text_snapshot.clone(),
 601                base_text,
 602                false,
 603                false,
 604                None,
 605                None,
 606                cx,
 607            )
 608            .await?;
 609            buffer_diff.update(cx, |diff, cx| {
 610                diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
 611            })?;
 612            this.update(cx, |this, cx| this.update_visible_ranges(cx))
 613        }));
 614    }
 615
 616    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
 617        self.revealed_ranges.push(range);
 618        self.update_visible_ranges(cx);
 619    }
 620
 621    fn update_visible_ranges(&mut self, cx: &mut Context<Self>) {
 622        let Some(buffer) = self.buffer.as_ref() else {
 623            return;
 624        };
 625
 626        let ranges = self.excerpt_ranges(cx);
 627        self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| {
 628            multibuffer.set_excerpts_for_path(
 629                PathKey::for_buffer(buffer, cx),
 630                buffer.clone(),
 631                ranges,
 632                editor::DEFAULT_MULTIBUFFER_CONTEXT,
 633                cx,
 634            );
 635            let end = multibuffer.len(cx);
 636            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
 637        });
 638        cx.notify();
 639    }
 640
 641    fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
 642        let Some(buffer) = self.buffer.as_ref() else {
 643            return Vec::new();
 644        };
 645        let Some(diff) = self.buffer_diff.as_ref() else {
 646            return Vec::new();
 647        };
 648
 649        let buffer = buffer.read(cx);
 650        let diff = diff.read(cx);
 651        let mut ranges = diff
 652            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
 653            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
 654            .collect::<Vec<_>>();
 655        ranges.extend(
 656            self.revealed_ranges
 657                .iter()
 658                .map(|range| range.to_point(&buffer)),
 659        );
 660        ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
 661
 662        // Merge adjacent ranges
 663        let mut ranges = ranges.into_iter().peekable();
 664        let mut merged_ranges = Vec::new();
 665        while let Some(mut range) = ranges.next() {
 666            while let Some(next_range) = ranges.peek() {
 667                if range.end >= next_range.start {
 668                    range.end = range.end.max(next_range.end);
 669                    ranges.next();
 670                } else {
 671                    break;
 672                }
 673            }
 674
 675            merged_ranges.push(range);
 676        }
 677        merged_ranges
 678    }
 679
 680    pub fn finalize(&mut self, cx: &mut Context<Self>) -> Result<()> {
 681        let ranges = self.excerpt_ranges(cx);
 682        let buffer = self.buffer.take().context("card was already finalized")?;
 683        let base_text = self
 684            .base_text
 685            .take()
 686            .context("card was already finalized")?;
 687        let language_registry = self.project.read(cx).languages().clone();
 688
 689        // Replace the buffer in the multibuffer with the snapshot
 690        let buffer = cx.new(|cx| {
 691            let language = buffer.read(cx).language().cloned();
 692            let buffer = TextBuffer::new_normalized(
 693                0,
 694                cx.entity_id().as_non_zero_u64().into(),
 695                buffer.read(cx).line_ending(),
 696                buffer.read(cx).as_rope().clone(),
 697            );
 698            let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
 699            buffer.set_language(language, cx);
 700            buffer
 701        });
 702
 703        let buffer_diff = cx.spawn({
 704            let buffer = buffer.clone();
 705            let language_registry = language_registry.clone();
 706            async move |_this, cx| {
 707                build_buffer_diff(base_text, &buffer, &language_registry, cx).await
 708            }
 709        });
 710
 711        cx.spawn(async move |this, cx| {
 712            let buffer_diff = buffer_diff.await?;
 713            this.update(cx, |this, cx| {
 714                this.multibuffer.update(cx, |multibuffer, cx| {
 715                    let path_key = PathKey::for_buffer(&buffer, cx);
 716                    multibuffer.clear(cx);
 717                    multibuffer.set_excerpts_for_path(
 718                        path_key,
 719                        buffer,
 720                        ranges,
 721                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
 722                        cx,
 723                    );
 724                    multibuffer.add_diff(buffer_diff.clone(), cx);
 725                });
 726
 727                cx.notify();
 728            })
 729        })
 730        .detach_and_log_err(cx);
 731        Ok(())
 732    }
 733}
 734
 735impl ToolCard for EditFileToolCard {
 736    fn render(
 737        &mut self,
 738        status: &ToolUseStatus,
 739        window: &mut Window,
 740        workspace: WeakEntity<Workspace>,
 741        cx: &mut Context<Self>,
 742    ) -> impl IntoElement {
 743        let error_message = match status {
 744            ToolUseStatus::Error(err) => Some(err),
 745            _ => None,
 746        };
 747
 748        let path_label_button = h_flex()
 749            .id(("edit-tool-path-label-button", self.editor.entity_id()))
 750            .w_full()
 751            .max_w_full()
 752            .px_1()
 753            .gap_0p5()
 754            .cursor_pointer()
 755            .rounded_sm()
 756            .opacity(0.8)
 757            .hover(|label| {
 758                label
 759                    .opacity(1.)
 760                    .bg(cx.theme().colors().element_hover.opacity(0.5))
 761            })
 762            .tooltip(Tooltip::text("Jump to File"))
 763            .child(
 764                h_flex()
 765                    .child(
 766                        Icon::new(IconName::Pencil)
 767                            .size(IconSize::XSmall)
 768                            .color(Color::Muted),
 769                    )
 770                    .child(
 771                        div()
 772                            .text_size(rems(0.8125))
 773                            .child(self.path.display().to_string())
 774                            .ml_1p5()
 775                            .mr_0p5(),
 776                    )
 777                    .child(
 778                        Icon::new(IconName::ArrowUpRight)
 779                            .size(IconSize::XSmall)
 780                            .color(Color::Ignored),
 781                    ),
 782            )
 783            .on_click({
 784                let path = self.path.clone();
 785                let workspace = workspace.clone();
 786                move |_, window, cx| {
 787                    workspace
 788                        .update(cx, {
 789                            |workspace, cx| {
 790                                let Some(project_path) =
 791                                    workspace.project().read(cx).find_project_path(&path, cx)
 792                                else {
 793                                    return;
 794                                };
 795                                let open_task =
 796                                    workspace.open_path(project_path, None, true, window, cx);
 797                                window
 798                                    .spawn(cx, async move |cx| {
 799                                        let item = open_task.await?;
 800                                        if let Some(active_editor) = item.downcast::<Editor>() {
 801                                            active_editor
 802                                                .update_in(cx, |editor, window, cx| {
 803                                                    editor.go_to_singleton_buffer_point(
 804                                                        language::Point::new(0, 0),
 805                                                        window,
 806                                                        cx,
 807                                                    );
 808                                                })
 809                                                .log_err();
 810                                        }
 811                                        anyhow::Ok(())
 812                                    })
 813                                    .detach_and_log_err(cx);
 814                            }
 815                        })
 816                        .ok();
 817                }
 818            })
 819            .into_any_element();
 820
 821        let codeblock_header_bg = cx
 822            .theme()
 823            .colors()
 824            .element_background
 825            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
 826
 827        let codeblock_header = h_flex()
 828            .flex_none()
 829            .p_1()
 830            .gap_1()
 831            .justify_between()
 832            .rounded_t_md()
 833            .when(error_message.is_none(), |header| {
 834                header.bg(codeblock_header_bg)
 835            })
 836            .child(path_label_button)
 837            .when_some(error_message, |header, error_message| {
 838                header.child(
 839                    h_flex()
 840                        .gap_1()
 841                        .child(
 842                            Icon::new(IconName::Close)
 843                                .size(IconSize::Small)
 844                                .color(Color::Error),
 845                        )
 846                        .child(
 847                            Disclosure::new(
 848                                ("edit-file-error-disclosure", self.editor.entity_id()),
 849                                self.error_expanded.is_some(),
 850                            )
 851                            .opened_icon(IconName::ChevronUp)
 852                            .closed_icon(IconName::ChevronDown)
 853                            .on_click(cx.listener({
 854                                let error_message = error_message.clone();
 855
 856                                move |this, _event, _window, cx| {
 857                                    if this.error_expanded.is_some() {
 858                                        this.error_expanded.take();
 859                                    } else {
 860                                        this.error_expanded = Some(cx.new(|cx| {
 861                                            Markdown::new(error_message.clone(), None, None, cx)
 862                                        }))
 863                                    }
 864                                    cx.notify();
 865                                }
 866                            })),
 867                        ),
 868                )
 869            })
 870            .when(error_message.is_none() && !self.is_loading(), |header| {
 871                header.child(
 872                    Disclosure::new(
 873                        ("edit-file-disclosure", self.editor.entity_id()),
 874                        self.preview_expanded,
 875                    )
 876                    .opened_icon(IconName::ChevronUp)
 877                    .closed_icon(IconName::ChevronDown)
 878                    .on_click(cx.listener(
 879                        move |this, _event, _window, _cx| {
 880                            this.preview_expanded = !this.preview_expanded;
 881                        },
 882                    )),
 883                )
 884            });
 885
 886        let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
 887            let line_height = editor
 888                .style()
 889                .map(|style| style.text.line_height_in_pixels(window.rem_size()))
 890                .unwrap_or_default();
 891
 892            editor.set_text_style_refinement(TextStyleRefinement {
 893                font_size: Some(
 894                    TextSize::Small
 895                        .rems(cx)
 896                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
 897                        .into(),
 898                ),
 899                ..TextStyleRefinement::default()
 900            });
 901            let element = editor.render(window, cx);
 902            (element.into_any_element(), line_height)
 903        });
 904
 905        let border_color = cx.theme().colors().border.opacity(0.6);
 906
 907        let waiting_for_diff = {
 908            let styles = [
 909                ("w_4_5", (0.1, 0.85), 2000),
 910                ("w_1_4", (0.2, 0.75), 2200),
 911                ("w_2_4", (0.15, 0.64), 1900),
 912                ("w_3_5", (0.25, 0.72), 2300),
 913                ("w_2_5", (0.3, 0.56), 1800),
 914            ];
 915
 916            let mut container = v_flex()
 917                .p_3()
 918                .gap_1()
 919                .border_t_1()
 920                .rounded_b_md()
 921                .border_color(border_color)
 922                .bg(cx.theme().colors().editor_background);
 923
 924            for (width_method, pulse_range, duration_ms) in styles.iter() {
 925                let (min_opacity, max_opacity) = *pulse_range;
 926                let placeholder = match *width_method {
 927                    "w_4_5" => div().w_3_4(),
 928                    "w_1_4" => div().w_1_4(),
 929                    "w_2_4" => div().w_2_4(),
 930                    "w_3_5" => div().w_3_5(),
 931                    "w_2_5" => div().w_2_5(),
 932                    _ => div().w_1_2(),
 933                }
 934                .id("loading_div")
 935                .h_1()
 936                .rounded_full()
 937                .bg(cx.theme().colors().element_active)
 938                .with_animation(
 939                    "loading_pulsate",
 940                    Animation::new(Duration::from_millis(*duration_ms))
 941                        .repeat()
 942                        .with_easing(pulsating_between(min_opacity, max_opacity)),
 943                    |label, delta| label.opacity(delta),
 944                );
 945
 946                container = container.child(placeholder);
 947            }
 948
 949            container
 950        };
 951
 952        v_flex()
 953            .mb_2()
 954            .border_1()
 955            .when(error_message.is_some(), |card| card.border_dashed())
 956            .border_color(border_color)
 957            .rounded_md()
 958            .overflow_hidden()
 959            .child(codeblock_header)
 960            .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
 961                card.child(
 962                    v_flex()
 963                        .p_2()
 964                        .gap_1()
 965                        .border_t_1()
 966                        .border_dashed()
 967                        .border_color(border_color)
 968                        .bg(cx.theme().colors().editor_background)
 969                        .rounded_b_md()
 970                        .child(
 971                            Label::new("Error")
 972                                .size(LabelSize::XSmall)
 973                                .color(Color::Error),
 974                        )
 975                        .child(
 976                            div()
 977                                .rounded_md()
 978                                .text_ui_sm(cx)
 979                                .bg(cx.theme().colors().editor_background)
 980                                .child(MarkdownElement::new(
 981                                    error_markdown.clone(),
 982                                    markdown_style(window, cx),
 983                                )),
 984                        ),
 985                )
 986            })
 987            .when(self.is_loading() && error_message.is_none(), |card| {
 988                card.child(waiting_for_diff)
 989            })
 990            .when(self.preview_expanded && !self.is_loading(), |card| {
 991                let editor_view = v_flex()
 992                    .relative()
 993                    .h_full()
 994                    .when(!self.full_height_expanded, |editor_container| {
 995                        editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0))
 996                    })
 997                    .overflow_hidden()
 998                    .border_t_1()
 999                    .border_color(border_color)
1000                    .bg(cx.theme().colors().editor_background)
1001                    .child(editor);
1002
1003                card.child(
1004                    ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id())
1005                        .with_total_lines(self.total_lines.unwrap_or(0) as usize)
1006                        .toggle_state(self.full_height_expanded)
1007                        .with_collapsed_fade()
1008                        .on_toggle({
1009                            let this = cx.entity().downgrade();
1010                            move |is_expanded, _window, cx| {
1011                                if let Some(this) = this.upgrade() {
1012                                    this.update(cx, |this, _cx| {
1013                                        this.full_height_expanded = is_expanded;
1014                                    });
1015                                }
1016                            }
1017                        }),
1018                )
1019            })
1020    }
1021}
1022
1023fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1024    let theme_settings = ThemeSettings::get_global(cx);
1025    let ui_font_size = TextSize::Default.rems(cx);
1026    let mut text_style = window.text_style();
1027
1028    text_style.refine(&TextStyleRefinement {
1029        font_family: Some(theme_settings.ui_font.family.clone()),
1030        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1031        font_features: Some(theme_settings.ui_font.features.clone()),
1032        font_size: Some(ui_font_size.into()),
1033        color: Some(cx.theme().colors().text),
1034        ..Default::default()
1035    });
1036
1037    MarkdownStyle {
1038        base_text_style: text_style.clone(),
1039        selection_background_color: cx.theme().players().local().selection,
1040        ..Default::default()
1041    }
1042}
1043
1044async fn build_buffer(
1045    mut text: String,
1046    path: Arc<Path>,
1047    language_registry: &Arc<language::LanguageRegistry>,
1048    cx: &mut AsyncApp,
1049) -> Result<Entity<Buffer>> {
1050    let line_ending = LineEnding::detect(&text);
1051    LineEnding::normalize(&mut text);
1052    let text = Rope::from(text);
1053    let language = cx
1054        .update(|_cx| language_registry.language_for_file_path(&path))?
1055        .await
1056        .ok();
1057    let buffer = cx.new(|cx| {
1058        let buffer = TextBuffer::new_normalized(
1059            0,
1060            cx.entity_id().as_non_zero_u64().into(),
1061            line_ending,
1062            text,
1063        );
1064        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
1065        buffer.set_language(language, cx);
1066        buffer
1067    })?;
1068    Ok(buffer)
1069}
1070
1071async fn build_buffer_diff(
1072    old_text: Arc<String>,
1073    buffer: &Entity<Buffer>,
1074    language_registry: &Arc<LanguageRegistry>,
1075    cx: &mut AsyncApp,
1076) -> Result<Entity<BufferDiff>> {
1077    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
1078
1079    let old_text_rope = cx
1080        .background_spawn({
1081            let old_text = old_text.clone();
1082            async move { Rope::from(old_text.as_str()) }
1083        })
1084        .await;
1085    let base_buffer = cx
1086        .update(|cx| {
1087            Buffer::build_snapshot(
1088                old_text_rope,
1089                buffer.language().cloned(),
1090                Some(language_registry.clone()),
1091                cx,
1092            )
1093        })?
1094        .await;
1095
1096    let diff_snapshot = cx
1097        .update(|cx| {
1098            BufferDiffSnapshot::new_with_base_buffer(
1099                buffer.text.clone(),
1100                Some(old_text),
1101                base_buffer,
1102                cx,
1103            )
1104        })?
1105        .await;
1106
1107    let secondary_diff = cx.new(|cx| {
1108        let mut diff = BufferDiff::new(&buffer, cx);
1109        diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
1110        diff
1111    })?;
1112
1113    cx.new(|cx| {
1114        let mut diff = BufferDiff::new(&buffer.text, cx);
1115        diff.set_snapshot(diff_snapshot, &buffer, cx);
1116        diff.set_secondary_diff(secondary_diff);
1117        diff
1118    })
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123    use super::*;
1124    use client::TelemetrySettings;
1125    use fs::{FakeFs, Fs};
1126    use gpui::{TestAppContext, UpdateGlobal};
1127    use language_model::fake_provider::FakeLanguageModel;
1128    use serde_json::json;
1129    use settings::SettingsStore;
1130    use util::path;
1131
1132    #[gpui::test]
1133    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
1134        init_test(cx);
1135
1136        let fs = FakeFs::new(cx.executor());
1137        fs.insert_tree("/root", json!({})).await;
1138        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1139        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1140        let model = Arc::new(FakeLanguageModel::default());
1141        let result = cx
1142            .update(|cx| {
1143                let input = serde_json::to_value(EditFileToolInput {
1144                    display_description: "Some edit".into(),
1145                    path: "root/nonexistent_file.txt".into(),
1146                    mode: EditFileMode::Edit,
1147                })
1148                .unwrap();
1149                Arc::new(EditFileTool)
1150                    .run(
1151                        input,
1152                        Arc::default(),
1153                        project.clone(),
1154                        action_log,
1155                        model,
1156                        None,
1157                        cx,
1158                    )
1159                    .output
1160            })
1161            .await;
1162        assert_eq!(
1163            result.unwrap_err().to_string(),
1164            "Can't edit file: path not found"
1165        );
1166    }
1167
1168    #[gpui::test]
1169    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
1170        let mode = &EditFileMode::Create;
1171
1172        let result = test_resolve_path(mode, "root/new.txt", cx);
1173        assert_resolved_path_eq(result.await, "new.txt");
1174
1175        let result = test_resolve_path(mode, "new.txt", cx);
1176        assert_resolved_path_eq(result.await, "new.txt");
1177
1178        let result = test_resolve_path(mode, "dir/new.txt", cx);
1179        assert_resolved_path_eq(result.await, "dir/new.txt");
1180
1181        let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
1182        assert_eq!(
1183            result.await.unwrap_err().to_string(),
1184            "Can't create file: file already exists"
1185        );
1186
1187        let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
1188        assert_eq!(
1189            result.await.unwrap_err().to_string(),
1190            "Can't create file: parent directory doesn't exist"
1191        );
1192    }
1193
1194    #[gpui::test]
1195    async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
1196        let mode = &EditFileMode::Edit;
1197
1198        let path_with_root = "root/dir/subdir/existing.txt";
1199        let path_without_root = "dir/subdir/existing.txt";
1200        let result = test_resolve_path(mode, path_with_root, cx);
1201        assert_resolved_path_eq(result.await, path_without_root);
1202
1203        let result = test_resolve_path(mode, path_without_root, cx);
1204        assert_resolved_path_eq(result.await, path_without_root);
1205
1206        let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
1207        assert_eq!(
1208            result.await.unwrap_err().to_string(),
1209            "Can't edit file: path not found"
1210        );
1211
1212        let result = test_resolve_path(mode, "root/dir", cx);
1213        assert_eq!(
1214            result.await.unwrap_err().to_string(),
1215            "Can't edit file: path is a directory"
1216        );
1217    }
1218
1219    async fn test_resolve_path(
1220        mode: &EditFileMode,
1221        path: &str,
1222        cx: &mut TestAppContext,
1223    ) -> anyhow::Result<ProjectPath> {
1224        init_test(cx);
1225
1226        let fs = FakeFs::new(cx.executor());
1227        fs.insert_tree(
1228            "/root",
1229            json!({
1230                "dir": {
1231                    "subdir": {
1232                        "existing.txt": "hello"
1233                    }
1234                }
1235            }),
1236        )
1237        .await;
1238        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1239
1240        let input = EditFileToolInput {
1241            display_description: "Some edit".into(),
1242            path: path.into(),
1243            mode: mode.clone(),
1244        };
1245
1246        let result = cx.update(|cx| resolve_path(&input, project, cx));
1247        result
1248    }
1249
1250    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
1251        let actual = path
1252            .expect("Should return valid path")
1253            .path
1254            .to_str()
1255            .unwrap()
1256            .replace("\\", "/"); // Naive Windows paths normalization
1257        assert_eq!(actual, expected);
1258    }
1259
1260    #[test]
1261    fn still_streaming_ui_text_with_path() {
1262        let input = json!({
1263            "path": "src/main.rs",
1264            "display_description": "",
1265            "old_string": "old code",
1266            "new_string": "new code"
1267        });
1268
1269        assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
1270    }
1271
1272    #[test]
1273    fn still_streaming_ui_text_with_description() {
1274        let input = json!({
1275            "path": "",
1276            "display_description": "Fix error handling",
1277            "old_string": "old code",
1278            "new_string": "new code"
1279        });
1280
1281        assert_eq!(
1282            EditFileTool.still_streaming_ui_text(&input),
1283            "Fix error handling",
1284        );
1285    }
1286
1287    #[test]
1288    fn still_streaming_ui_text_with_path_and_description() {
1289        let input = json!({
1290            "path": "src/main.rs",
1291            "display_description": "Fix error handling",
1292            "old_string": "old code",
1293            "new_string": "new code"
1294        });
1295
1296        assert_eq!(
1297            EditFileTool.still_streaming_ui_text(&input),
1298            "Fix error handling",
1299        );
1300    }
1301
1302    #[test]
1303    fn still_streaming_ui_text_no_path_or_description() {
1304        let input = json!({
1305            "path": "",
1306            "display_description": "",
1307            "old_string": "old code",
1308            "new_string": "new code"
1309        });
1310
1311        assert_eq!(
1312            EditFileTool.still_streaming_ui_text(&input),
1313            DEFAULT_UI_TEXT,
1314        );
1315    }
1316
1317    #[test]
1318    fn still_streaming_ui_text_with_null() {
1319        let input = serde_json::Value::Null;
1320
1321        assert_eq!(
1322            EditFileTool.still_streaming_ui_text(&input),
1323            DEFAULT_UI_TEXT,
1324        );
1325    }
1326
1327    fn init_test(cx: &mut TestAppContext) {
1328        cx.update(|cx| {
1329            let settings_store = SettingsStore::test(cx);
1330            cx.set_global(settings_store);
1331            language::init(cx);
1332            TelemetrySettings::register(cx);
1333            Project::init_settings(cx);
1334        });
1335    }
1336
1337    #[gpui::test]
1338    async fn test_format_on_save(cx: &mut TestAppContext) {
1339        init_test(cx);
1340
1341        let fs = FakeFs::new(cx.executor());
1342        fs.insert_tree("/root", json!({"src": {}})).await;
1343
1344        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1345
1346        // Set up a Rust language with LSP formatting support
1347        let rust_language = Arc::new(language::Language::new(
1348            language::LanguageConfig {
1349                name: "Rust".into(),
1350                matcher: language::LanguageMatcher {
1351                    path_suffixes: vec!["rs".to_string()],
1352                    ..Default::default()
1353                },
1354                ..Default::default()
1355            },
1356            None,
1357        ));
1358
1359        // Register the language and fake LSP
1360        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1361        language_registry.add(rust_language);
1362
1363        let mut fake_language_servers = language_registry.register_fake_lsp(
1364            "Rust",
1365            language::FakeLspAdapter {
1366                capabilities: lsp::ServerCapabilities {
1367                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
1368                    ..Default::default()
1369                },
1370                ..Default::default()
1371            },
1372        );
1373
1374        // Create the file
1375        fs.save(
1376            path!("/root/src/main.rs").as_ref(),
1377            &"initial content".into(),
1378            language::LineEnding::Unix,
1379        )
1380        .await
1381        .unwrap();
1382
1383        // Open the buffer to trigger LSP initialization
1384        let buffer = project
1385            .update(cx, |project, cx| {
1386                project.open_local_buffer(path!("/root/src/main.rs"), cx)
1387            })
1388            .await
1389            .unwrap();
1390
1391        // Register the buffer with language servers
1392        let _handle = project.update(cx, |project, cx| {
1393            project.register_buffer_with_language_servers(&buffer, cx)
1394        });
1395
1396        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
1397        const FORMATTED_CONTENT: &str =
1398            "This file was formatted by the fake formatter in the test.\n";
1399
1400        // Get the fake language server and set up formatting handler
1401        let fake_language_server = fake_language_servers.next().await.unwrap();
1402        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
1403            |_, _| async move {
1404                Ok(Some(vec![lsp::TextEdit {
1405                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
1406                    new_text: FORMATTED_CONTENT.to_string(),
1407                }]))
1408            }
1409        });
1410
1411        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1412        let model = Arc::new(FakeLanguageModel::default());
1413
1414        // First, test with format_on_save enabled
1415        cx.update(|cx| {
1416            SettingsStore::update_global(cx, |store, cx| {
1417                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1418                    cx,
1419                    |settings| {
1420                        settings.defaults.format_on_save = Some(FormatOnSave::On);
1421                        settings.defaults.formatter =
1422                            Some(language::language_settings::SelectedFormatter::Auto);
1423                    },
1424                );
1425            });
1426        });
1427
1428        // Have the model stream unformatted content
1429        let edit_result = {
1430            let edit_task = cx.update(|cx| {
1431                let input = serde_json::to_value(EditFileToolInput {
1432                    display_description: "Create main function".into(),
1433                    path: "root/src/main.rs".into(),
1434                    mode: EditFileMode::Overwrite,
1435                })
1436                .unwrap();
1437                Arc::new(EditFileTool)
1438                    .run(
1439                        input,
1440                        Arc::default(),
1441                        project.clone(),
1442                        action_log.clone(),
1443                        model.clone(),
1444                        None,
1445                        cx,
1446                    )
1447                    .output
1448            });
1449
1450            // Stream the unformatted content
1451            cx.executor().run_until_parked();
1452            model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1453            model.end_last_completion_stream();
1454
1455            edit_task.await
1456        };
1457        assert!(edit_result.is_ok());
1458
1459        // Wait for any async operations (e.g. formatting) to complete
1460        cx.executor().run_until_parked();
1461
1462        // Read the file to verify it was formatted automatically
1463        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1464        assert_eq!(
1465            // Ignore carriage returns on Windows
1466            new_content.replace("\r\n", "\n"),
1467            FORMATTED_CONTENT,
1468            "Code should be formatted when format_on_save is enabled"
1469        );
1470
1471        let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
1472
1473        assert_eq!(
1474            stale_buffer_count, 0,
1475            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
1476             This causes the agent to think the file was modified externally when it was just formatted.",
1477            stale_buffer_count
1478        );
1479
1480        // Next, test with format_on_save disabled
1481        cx.update(|cx| {
1482            SettingsStore::update_global(cx, |store, cx| {
1483                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1484                    cx,
1485                    |settings| {
1486                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
1487                    },
1488                );
1489            });
1490        });
1491
1492        // Stream unformatted edits again
1493        let edit_result = {
1494            let edit_task = cx.update(|cx| {
1495                let input = serde_json::to_value(EditFileToolInput {
1496                    display_description: "Update main function".into(),
1497                    path: "root/src/main.rs".into(),
1498                    mode: EditFileMode::Overwrite,
1499                })
1500                .unwrap();
1501                Arc::new(EditFileTool)
1502                    .run(
1503                        input,
1504                        Arc::default(),
1505                        project.clone(),
1506                        action_log.clone(),
1507                        model.clone(),
1508                        None,
1509                        cx,
1510                    )
1511                    .output
1512            });
1513
1514            // Stream the unformatted content
1515            cx.executor().run_until_parked();
1516            model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1517            model.end_last_completion_stream();
1518
1519            edit_task.await
1520        };
1521        assert!(edit_result.is_ok());
1522
1523        // Wait for any async operations (e.g. formatting) to complete
1524        cx.executor().run_until_parked();
1525
1526        // Verify the file was not formatted
1527        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1528        assert_eq!(
1529            // Ignore carriage returns on Windows
1530            new_content.replace("\r\n", "\n"),
1531            UNFORMATTED_CONTENT,
1532            "Code should not be formatted when format_on_save is disabled"
1533        );
1534    }
1535
1536    #[gpui::test]
1537    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
1538        init_test(cx);
1539
1540        let fs = FakeFs::new(cx.executor());
1541        fs.insert_tree("/root", json!({"src": {}})).await;
1542
1543        // Create a simple file with trailing whitespace
1544        fs.save(
1545            path!("/root/src/main.rs").as_ref(),
1546            &"initial content".into(),
1547            language::LineEnding::Unix,
1548        )
1549        .await
1550        .unwrap();
1551
1552        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1553        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1554        let model = Arc::new(FakeLanguageModel::default());
1555
1556        // First, test with remove_trailing_whitespace_on_save enabled
1557        cx.update(|cx| {
1558            SettingsStore::update_global(cx, |store, cx| {
1559                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1560                    cx,
1561                    |settings| {
1562                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
1563                    },
1564                );
1565            });
1566        });
1567
1568        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
1569            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
1570
1571        // Have the model stream content that contains trailing whitespace
1572        let edit_result = {
1573            let edit_task = cx.update(|cx| {
1574                let input = serde_json::to_value(EditFileToolInput {
1575                    display_description: "Create main function".into(),
1576                    path: "root/src/main.rs".into(),
1577                    mode: EditFileMode::Overwrite,
1578                })
1579                .unwrap();
1580                Arc::new(EditFileTool)
1581                    .run(
1582                        input,
1583                        Arc::default(),
1584                        project.clone(),
1585                        action_log.clone(),
1586                        model.clone(),
1587                        None,
1588                        cx,
1589                    )
1590                    .output
1591            });
1592
1593            // Stream the content with trailing whitespace
1594            cx.executor().run_until_parked();
1595            model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1596            model.end_last_completion_stream();
1597
1598            edit_task.await
1599        };
1600        assert!(edit_result.is_ok());
1601
1602        // Wait for any async operations (e.g. formatting) to complete
1603        cx.executor().run_until_parked();
1604
1605        // Read the file to verify trailing whitespace was removed automatically
1606        assert_eq!(
1607            // Ignore carriage returns on Windows
1608            fs.load(path!("/root/src/main.rs").as_ref())
1609                .await
1610                .unwrap()
1611                .replace("\r\n", "\n"),
1612            "fn main() {\n    println!(\"Hello!\");\n}\n",
1613            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
1614        );
1615
1616        // Next, test with remove_trailing_whitespace_on_save disabled
1617        cx.update(|cx| {
1618            SettingsStore::update_global(cx, |store, cx| {
1619                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
1620                    cx,
1621                    |settings| {
1622                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
1623                    },
1624                );
1625            });
1626        });
1627
1628        // Stream edits again with trailing whitespace
1629        let edit_result = {
1630            let edit_task = cx.update(|cx| {
1631                let input = serde_json::to_value(EditFileToolInput {
1632                    display_description: "Update main function".into(),
1633                    path: "root/src/main.rs".into(),
1634                    mode: EditFileMode::Overwrite,
1635                })
1636                .unwrap();
1637                Arc::new(EditFileTool)
1638                    .run(
1639                        input,
1640                        Arc::default(),
1641                        project.clone(),
1642                        action_log.clone(),
1643                        model.clone(),
1644                        None,
1645                        cx,
1646                    )
1647                    .output
1648            });
1649
1650            // Stream the content with trailing whitespace
1651            cx.executor().run_until_parked();
1652            model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1653            model.end_last_completion_stream();
1654
1655            edit_task.await
1656        };
1657        assert!(edit_result.is_ok());
1658
1659        // Wait for any async operations (e.g. formatting) to complete
1660        cx.executor().run_until_parked();
1661
1662        // Verify the file still has trailing whitespace
1663        // Read the file again - it should still have trailing whitespace
1664        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1665        assert_eq!(
1666            // Ignore carriage returns on Windows
1667            final_content.replace("\r\n", "\n"),
1668            CONTENT_WITH_TRAILING_WHITESPACE,
1669            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
1670        );
1671    }
1672}