edit_file_tool.rs

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