edit_file_tool.rs

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