edit_file_tool.rs

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