edit_file_tool.rs

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