edit_file_tool.rs

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