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