edit_file_tool.rs

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