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