streaming_edit_file_tool.rs

   1use super::edit_file_tool::EditFileTool;
   2use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;
   3use super::save_file_tool::SaveFileTool;
   4use super::tool_edit_parser::{ToolEditEvent, ToolEditParser};
   5use crate::{
   6    AgentTool, Thread, ToolCallEventStream, ToolInput,
   7    edit_agent::{
   8        reindent::{Reindenter, compute_indent_delta},
   9        streaming_fuzzy_matcher::StreamingFuzzyMatcher,
  10    },
  11};
  12use acp_thread::Diff;
  13use action_log::ActionLog;
  14use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
  15use anyhow::{Context as _, Result};
  16use collections::HashSet;
  17use futures::FutureExt as _;
  18use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
  19use language::language_settings::{self, FormatOnSave};
  20use language::{Buffer, LanguageRegistry};
  21use language_model::LanguageModelToolResultContent;
  22use project::lsp_store::{FormatTrigger, LspFormatTarget};
  23use project::{AgentLocation, Project, ProjectPath};
  24use schemars::JsonSchema;
  25use serde::{Deserialize, Serialize};
  26use std::ops::Range;
  27use std::path::PathBuf;
  28use std::sync::Arc;
  29use streaming_diff::{CharOperation, StreamingDiff};
  30use text::ToOffset;
  31use ui::SharedString;
  32use util::rel_path::RelPath;
  33use util::{Deferred, ResultExt};
  34
  35const DEFAULT_UI_TEXT: &str = "Editing file";
  36
  37/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `move_path` tool instead.
  38///
  39/// Before using this tool:
  40///
  41/// 1. Use the `read_file` tool to understand the file's contents and context
  42///
  43/// 2. Verify the directory path is correct (only applicable when creating new files):
  44///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location
  45#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  46pub struct StreamingEditFileToolInput {
  47    /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI.
  48    ///
  49    /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
  50    ///
  51    /// NEVER mention the file path in this description.
  52    ///
  53    /// <example>Fix API endpoint URLs</example>
  54    /// <example>Update copyright year in `page_footer`</example>
  55    ///
  56    /// Make sure to include this field before all the others in the input object so that we can display it immediately.
  57    pub display_description: String,
  58
  59    /// The full path of the file to create or modify in the project.
  60    ///
  61    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
  62    ///
  63    /// The following examples assume we have two root directories in the project:
  64    /// - /a/b/backend
  65    /// - /c/d/frontend
  66    ///
  67    /// <example>
  68    /// `backend/src/main.rs`
  69    ///
  70    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
  71    /// </example>
  72    ///
  73    /// <example>
  74    /// `frontend/db.js`
  75    /// </example>
  76    pub path: String,
  77
  78    /// The mode of operation on the file. Possible values:
  79    /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. Requires 'content' field.
  80    /// - 'edit': Make granular edits to an existing file. Requires 'edits' field.
  81    ///
  82    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
  83    pub mode: StreamingEditFileMode,
  84
  85    /// The complete content for the new file (required for 'write' mode).
  86    /// This field should contain the entire file content.
  87    #[serde(default, skip_serializing_if = "Option::is_none")]
  88    pub content: Option<String>,
  89
  90    /// List of edit operations to apply sequentially (required for 'edit' mode).
  91    /// Each edit finds `old_text` in the file and replaces it with `new_text`.
  92    #[serde(default, skip_serializing_if = "Option::is_none")]
  93    pub edits: Option<Vec<Edit>>,
  94}
  95
  96#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  97#[serde(rename_all = "snake_case")]
  98pub enum StreamingEditFileMode {
  99    /// Overwrite the file with new content (replacing any existing content).
 100    /// If the file does not exist, it will be created.
 101    Write,
 102    /// Make granular edits to an existing file
 103    Edit,
 104}
 105
 106/// A single edit operation that replaces old text with new text
 107#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 108pub struct Edit {
 109    /// The exact text to find in the file. This will be matched using fuzzy matching
 110    /// to handle minor differences in whitespace or formatting.
 111    pub old_text: String,
 112    /// The text to replace it with
 113    pub new_text: String,
 114}
 115
 116#[derive(Default, Debug, Deserialize)]
 117struct StreamingEditFileToolPartialInput {
 118    #[serde(default)]
 119    display_description: Option<String>,
 120    #[serde(default)]
 121    path: Option<String>,
 122    #[serde(default)]
 123    mode: Option<StreamingEditFileMode>,
 124    #[serde(default)]
 125    content: Option<String>,
 126    #[serde(default)]
 127    edits: Option<Vec<PartialEdit>>,
 128}
 129
 130#[derive(Default, Debug, Deserialize)]
 131pub struct PartialEdit {
 132    #[serde(default)]
 133    pub old_text: Option<String>,
 134    #[serde(default)]
 135    pub new_text: Option<String>,
 136}
 137
 138#[derive(Debug, Serialize, Deserialize)]
 139#[serde(untagged)]
 140pub enum StreamingEditFileToolOutput {
 141    Success {
 142        #[serde(alias = "original_path")]
 143        input_path: PathBuf,
 144        new_text: String,
 145        old_text: Arc<String>,
 146        #[serde(default)]
 147        diff: String,
 148    },
 149    Error {
 150        error: String,
 151    },
 152}
 153
 154impl StreamingEditFileToolOutput {
 155    pub fn error(error: impl Into<String>) -> Self {
 156        Self::Error {
 157            error: error.into(),
 158        }
 159    }
 160}
 161
 162impl std::fmt::Display for StreamingEditFileToolOutput {
 163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 164        match self {
 165            StreamingEditFileToolOutput::Success {
 166                diff, input_path, ..
 167            } => {
 168                if diff.is_empty() {
 169                    write!(f, "No edits were made.")
 170                } else {
 171                    write!(
 172                        f,
 173                        "Edited {}:\n\n```diff\n{diff}\n```",
 174                        input_path.display()
 175                    )
 176                }
 177            }
 178            StreamingEditFileToolOutput::Error { error } => write!(f, "{error}"),
 179        }
 180    }
 181}
 182
 183impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent {
 184    fn from(output: StreamingEditFileToolOutput) -> Self {
 185        output.to_string().into()
 186    }
 187}
 188
 189pub struct StreamingEditFileTool {
 190    thread: WeakEntity<Thread>,
 191    language_registry: Arc<LanguageRegistry>,
 192    project: Entity<Project>,
 193}
 194
 195impl StreamingEditFileTool {
 196    pub fn new(
 197        project: Entity<Project>,
 198        thread: WeakEntity<Thread>,
 199        language_registry: Arc<LanguageRegistry>,
 200    ) -> Self {
 201        Self {
 202            project,
 203            thread,
 204            language_registry,
 205        }
 206    }
 207
 208    fn authorize(
 209        &self,
 210        path: &PathBuf,
 211        description: &str,
 212        event_stream: &ToolCallEventStream,
 213        cx: &mut App,
 214    ) -> Task<Result<()>> {
 215        super::tool_permissions::authorize_file_edit(
 216            EditFileTool::NAME,
 217            path,
 218            description,
 219            &self.thread,
 220            event_stream,
 221            cx,
 222        )
 223    }
 224
 225    fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) {
 226        let should_update_agent_location = self
 227            .thread
 228            .read_with(cx, |thread, _cx| !thread.is_subagent())
 229            .unwrap_or_default();
 230        if should_update_agent_location {
 231            self.project.update(cx, |project, cx| {
 232                project.set_agent_location(Some(AgentLocation { buffer, position }), cx);
 233            });
 234        }
 235    }
 236}
 237
 238impl AgentTool for StreamingEditFileTool {
 239    type Input = StreamingEditFileToolInput;
 240    type Output = StreamingEditFileToolOutput;
 241
 242    const NAME: &'static str = "streaming_edit_file";
 243
 244    fn supports_input_streaming() -> bool {
 245        true
 246    }
 247
 248    fn kind() -> acp::ToolKind {
 249        acp::ToolKind::Edit
 250    }
 251
 252    fn initial_title(
 253        &self,
 254        input: Result<Self::Input, serde_json::Value>,
 255        cx: &mut App,
 256    ) -> SharedString {
 257        match input {
 258            Ok(input) => self
 259                .project
 260                .read(cx)
 261                .find_project_path(&input.path, cx)
 262                .and_then(|project_path| {
 263                    self.project
 264                        .read(cx)
 265                        .short_full_path_for_project_path(&project_path, cx)
 266                })
 267                .unwrap_or(input.path)
 268                .into(),
 269            Err(raw_input) => {
 270                if let Some(input) =
 271                    serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input).ok()
 272                {
 273                    let path = input.path.unwrap_or_default();
 274                    let path = path.trim();
 275                    if !path.is_empty() {
 276                        return self
 277                            .project
 278                            .read(cx)
 279                            .find_project_path(&path, cx)
 280                            .and_then(|project_path| {
 281                                self.project
 282                                    .read(cx)
 283                                    .short_full_path_for_project_path(&project_path, cx)
 284                            })
 285                            .unwrap_or_else(|| path.to_string())
 286                            .into();
 287                    }
 288
 289                    let description = input.display_description.unwrap_or_default();
 290                    let description = description.trim();
 291                    if !description.is_empty() {
 292                        return description.to_string().into();
 293                    }
 294                }
 295
 296                DEFAULT_UI_TEXT.into()
 297            }
 298        }
 299    }
 300
 301    fn run(
 302        self: Arc<Self>,
 303        mut input: ToolInput<Self::Input>,
 304        event_stream: ToolCallEventStream,
 305        cx: &mut App,
 306    ) -> Task<Result<Self::Output, Self::Output>> {
 307        cx.spawn(async move |cx: &mut AsyncApp| {
 308            let mut state: Option<EditSession> = None;
 309            loop {
 310                futures::select! {
 311                    partial = input.recv_partial().fuse() => {
 312                        let Some(partial_value) = partial else { break };
 313                        if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial_value) {
 314                            if state.is_none() && let Some(path_str) = &parsed.path
 315                                && let Some(display_description) = &parsed.display_description
 316                                && let Some(mode) = parsed.mode.clone() {
 317                                    state = Some(
 318                                        EditSession::new(
 319                                            path_str,
 320                                            display_description,
 321                                            mode,
 322                                            &self,
 323                                            &event_stream,
 324                                            cx,
 325                                        )
 326                                        .await?,
 327                                    );
 328                            }
 329
 330                            if let Some(state) = &mut state {
 331                                state.process(parsed, &self, &event_stream, cx)?;
 332                            }
 333                        }
 334                    }
 335                    _ = event_stream.cancelled_by_user().fuse() => {
 336                        return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 337                    }
 338                }
 339            }
 340            let full_input =
 341                input
 342                    .recv()
 343                    .await
 344                    .map_err(|e| StreamingEditFileToolOutput::error(format!("Failed to receive tool input: {e}")))?;
 345
 346            let mut state = if let Some(state) = state {
 347                state
 348            } else {
 349                EditSession::new(
 350                    &full_input.path,
 351                    &full_input.display_description,
 352                    full_input.mode.clone(),
 353                    &self,
 354                    &event_stream,
 355                    cx,
 356                )
 357                .await?
 358            };
 359            state.finalize(full_input, &self, &event_stream, cx).await
 360        })
 361    }
 362
 363    fn replay(
 364        &self,
 365        _input: Self::Input,
 366        output: Self::Output,
 367        event_stream: ToolCallEventStream,
 368        cx: &mut App,
 369    ) -> Result<()> {
 370        match output {
 371            StreamingEditFileToolOutput::Success {
 372                input_path,
 373                old_text,
 374                new_text,
 375                ..
 376            } => {
 377                event_stream.update_diff(cx.new(|cx| {
 378                    Diff::finalized(
 379                        input_path.to_string_lossy().into_owned(),
 380                        Some(old_text.to_string()),
 381                        new_text,
 382                        self.language_registry.clone(),
 383                        cx,
 384                    )
 385                }));
 386                Ok(())
 387            }
 388            StreamingEditFileToolOutput::Error { .. } => Ok(()),
 389        }
 390    }
 391}
 392
 393pub struct EditSession {
 394    abs_path: PathBuf,
 395    buffer: Entity<Buffer>,
 396    old_text: Arc<String>,
 397    diff: Entity<Diff>,
 398    mode: StreamingEditFileMode,
 399    parser: ToolEditParser,
 400    pipeline: EditPipeline,
 401    _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>,
 402}
 403
 404struct EditPipeline {
 405    edits: Vec<EditPipelineEntry>,
 406    content_written: bool,
 407}
 408
 409enum EditPipelineEntry {
 410    ResolvingOldText {
 411        matcher: StreamingFuzzyMatcher,
 412    },
 413    StreamingNewText {
 414        streaming_diff: StreamingDiff,
 415        edit_cursor: usize,
 416        reindenter: Reindenter,
 417        original_snapshot: text::BufferSnapshot,
 418    },
 419    Done,
 420}
 421
 422impl EditPipeline {
 423    fn new() -> Self {
 424        Self {
 425            edits: Vec::new(),
 426            content_written: false,
 427        }
 428    }
 429
 430    fn ensure_resolving_old_text(
 431        &mut self,
 432        edit_index: usize,
 433        buffer: &Entity<Buffer>,
 434        cx: &mut AsyncApp,
 435    ) {
 436        while self.edits.len() <= edit_index {
 437            let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());
 438            self.edits.push(EditPipelineEntry::ResolvingOldText {
 439                matcher: StreamingFuzzyMatcher::new(snapshot),
 440            });
 441        }
 442    }
 443}
 444
 445/// Compute the `LineIndent` of the first line in a set of query lines.
 446fn query_first_line_indent(query_lines: &[String]) -> text::LineIndent {
 447    let first_line = query_lines.first().map(|s| s.as_str()).unwrap_or("");
 448    text::LineIndent::from_iter(first_line.chars())
 449}
 450
 451impl EditSession {
 452    async fn new(
 453        path_str: &str,
 454        display_description: &str,
 455        mode: StreamingEditFileMode,
 456        tool: &StreamingEditFileTool,
 457        event_stream: &ToolCallEventStream,
 458        cx: &mut AsyncApp,
 459    ) -> Result<Self, StreamingEditFileToolOutput> {
 460        let path = PathBuf::from(path_str);
 461        let project_path = cx
 462            .update(|cx| resolve_path(mode.clone(), &path, &tool.project, cx))
 463            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 464
 465        let Some(abs_path) = cx.update(|cx| tool.project.read(cx).absolute_path(&project_path, cx))
 466        else {
 467            return Err(StreamingEditFileToolOutput::error(format!(
 468                "Worktree at '{path_str}' does not exist"
 469            )));
 470        };
 471
 472        event_stream.update_fields(
 473            ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path.clone())]),
 474        );
 475
 476        cx.update(|cx| tool.authorize(&path, &display_description, event_stream, cx))
 477            .await
 478            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 479
 480        let buffer = tool
 481            .project
 482            .update(cx, |project, cx| project.open_buffer(project_path, cx))
 483            .await
 484            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 485
 486        let action_log = tool
 487            .thread
 488            .read_with(cx, |thread, _cx| thread.action_log().clone())
 489            .ok();
 490
 491        ensure_buffer_saved(&buffer, &abs_path, tool, action_log.as_ref(), cx)?;
 492
 493        let diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
 494        event_stream.update_diff(diff.clone());
 495        let finalize_diff_guard = util::defer(Box::new({
 496            let diff = diff.downgrade();
 497            let mut cx = cx.clone();
 498            move || {
 499                diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
 500            }
 501        }) as Box<dyn FnOnce()>);
 502
 503        if let Some(action_log) = &action_log {
 504            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 505        }
 506
 507        let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 508        let old_text = cx
 509            .background_spawn({
 510                let old_snapshot = old_snapshot.clone();
 511                async move { Arc::new(old_snapshot.text()) }
 512            })
 513            .await;
 514
 515        Ok(Self {
 516            abs_path,
 517            buffer,
 518            old_text,
 519            diff,
 520            mode,
 521            parser: ToolEditParser::default(),
 522            pipeline: EditPipeline::new(),
 523            _finalize_diff_guard: finalize_diff_guard,
 524        })
 525    }
 526
 527    async fn finalize(
 528        &mut self,
 529        input: StreamingEditFileToolInput,
 530        tool: &StreamingEditFileTool,
 531        event_stream: &ToolCallEventStream,
 532        cx: &mut AsyncApp,
 533    ) -> Result<StreamingEditFileToolOutput, StreamingEditFileToolOutput> {
 534        let Self {
 535            buffer,
 536            old_text,
 537            diff,
 538            abs_path,
 539            parser,
 540            pipeline,
 541            ..
 542        } = self;
 543
 544        let action_log = tool
 545            .thread
 546            .read_with(cx, |thread, _cx| thread.action_log().clone())
 547            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 548
 549        match input.mode {
 550            StreamingEditFileMode::Write => {
 551                action_log.update(cx, |log, cx| {
 552                    log.buffer_created(buffer.clone(), cx);
 553                });
 554                let content = input.content.ok_or_else(|| {
 555                    StreamingEditFileToolOutput::error("'content' field is required for write mode")
 556                })?;
 557
 558                let events = parser.finalize_content(&content);
 559                Self::process_events(
 560                    &events,
 561                    buffer,
 562                    diff,
 563                    pipeline,
 564                    abs_path,
 565                    tool,
 566                    event_stream,
 567                    cx,
 568                )?;
 569            }
 570            StreamingEditFileMode::Edit => {
 571                let edits = input.edits.ok_or_else(|| {
 572                    StreamingEditFileToolOutput::error("'edits' field is required for edit mode")
 573                })?;
 574
 575                let final_edits = edits
 576                    .into_iter()
 577                    .map(|e| Edit {
 578                        old_text: e.old_text,
 579                        new_text: e.new_text,
 580                    })
 581                    .collect::<Vec<_>>();
 582                let events = parser.finalize_edits(&final_edits);
 583                Self::process_events(
 584                    &events,
 585                    buffer,
 586                    diff,
 587                    pipeline,
 588                    abs_path,
 589                    tool,
 590                    event_stream,
 591                    cx,
 592                )?;
 593            }
 594        }
 595
 596        let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| {
 597            let settings = language_settings::language_settings(
 598                buffer.language().map(|l| l.name()),
 599                buffer.file(),
 600                cx,
 601            );
 602            settings.format_on_save != FormatOnSave::Off
 603        });
 604
 605        if format_on_save_enabled {
 606            action_log.update(cx, |log, cx| {
 607                log.buffer_edited(buffer.clone(), cx);
 608            });
 609
 610            let format_task = tool.project.update(cx, |project, cx| {
 611                project.format(
 612                    HashSet::from_iter([buffer.clone()]),
 613                    LspFormatTarget::Buffers,
 614                    false,
 615                    FormatTrigger::Save,
 616                    cx,
 617                )
 618            });
 619            futures::select! {
 620                result = format_task.fuse() => { result.log_err(); },
 621                _ = event_stream.cancelled_by_user().fuse() => {
 622                    return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 623                }
 624            };
 625        }
 626
 627        let save_task = tool
 628            .project
 629            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 630        futures::select! {
 631            result = save_task.fuse() => { result.map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; },
 632            _ = event_stream.cancelled_by_user().fuse() => {
 633                return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 634            }
 635        };
 636
 637        action_log.update(cx, |log, cx| {
 638            log.buffer_edited(buffer.clone(), cx);
 639        });
 640
 641        let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 642        let (new_text, unified_diff) = cx
 643            .background_spawn({
 644                let new_snapshot = new_snapshot.clone();
 645                let old_text = old_text.clone();
 646                async move {
 647                    let new_text = new_snapshot.text();
 648                    let diff = language::unified_diff(&old_text, &new_text);
 649                    (new_text, diff)
 650                }
 651            })
 652            .await;
 653
 654        let output = StreamingEditFileToolOutput::Success {
 655            input_path: PathBuf::from(input.path),
 656            new_text,
 657            old_text: old_text.clone(),
 658            diff: unified_diff,
 659        };
 660        Ok(output)
 661    }
 662
 663    fn process(
 664        &mut self,
 665        partial: StreamingEditFileToolPartialInput,
 666        tool: &StreamingEditFileTool,
 667        event_stream: &ToolCallEventStream,
 668        cx: &mut AsyncApp,
 669    ) -> Result<(), StreamingEditFileToolOutput> {
 670        match &self.mode {
 671            StreamingEditFileMode::Write => {
 672                if let Some(content) = &partial.content {
 673                    let events = self.parser.push_content(content);
 674                    Self::process_events(
 675                        &events,
 676                        &self.buffer,
 677                        &self.diff,
 678                        &mut self.pipeline,
 679                        &self.abs_path,
 680                        tool,
 681                        event_stream,
 682                        cx,
 683                    )?;
 684                }
 685            }
 686            StreamingEditFileMode::Edit => {
 687                if let Some(edits) = partial.edits {
 688                    let events = self.parser.push_edits(&edits);
 689                    Self::process_events(
 690                        &events,
 691                        &self.buffer,
 692                        &self.diff,
 693                        &mut self.pipeline,
 694                        &self.abs_path,
 695                        tool,
 696                        event_stream,
 697                        cx,
 698                    )?;
 699                }
 700            }
 701        }
 702        Ok(())
 703    }
 704
 705    fn process_events(
 706        events: &[ToolEditEvent],
 707        buffer: &Entity<Buffer>,
 708        diff: &Entity<Diff>,
 709        pipeline: &mut EditPipeline,
 710        abs_path: &PathBuf,
 711        tool: &StreamingEditFileTool,
 712        event_stream: &ToolCallEventStream,
 713        cx: &mut AsyncApp,
 714    ) -> Result<(), StreamingEditFileToolOutput> {
 715        let action_log = tool
 716            .thread
 717            .read_with(cx, |thread, _cx| thread.action_log().clone())
 718            .ok();
 719
 720        for event in events {
 721            match event {
 722                ToolEditEvent::ContentChunk { chunk } => {
 723                    let (buffer_id, insert_at) = buffer.read_with(cx, |buffer, _cx| {
 724                        let insert_at = if !pipeline.content_written && buffer.len() > 0 {
 725                            0..buffer.len()
 726                        } else {
 727                            let len = buffer.len();
 728                            len..len
 729                        };
 730                        (buffer.remote_id(), insert_at)
 731                    });
 732                    agent_edit_buffer(
 733                        buffer,
 734                        [(insert_at, chunk.as_str())],
 735                        action_log.as_ref(),
 736                        cx,
 737                    );
 738                    cx.update(|cx| {
 739                        tool.set_agent_location(
 740                            buffer.downgrade(),
 741                            text::Anchor::max_for_buffer(buffer_id),
 742                            cx,
 743                        );
 744                    });
 745                    pipeline.content_written = true;
 746                }
 747
 748                ToolEditEvent::OldTextChunk {
 749                    edit_index,
 750                    chunk,
 751                    done: false,
 752                } => {
 753                    pipeline.ensure_resolving_old_text(*edit_index, buffer, cx);
 754
 755                    if let EditPipelineEntry::ResolvingOldText { matcher } =
 756                        &mut pipeline.edits[*edit_index]
 757                    {
 758                        if !chunk.is_empty() {
 759                            if let Some(match_range) = matcher.push(chunk, None) {
 760                                let anchor_range = buffer.read_with(cx, |buffer, _cx| {
 761                                    buffer.anchor_range_between(match_range.clone())
 762                                });
 763                                diff.update(cx, |diff, cx| diff.reveal_range(anchor_range, cx));
 764
 765                                cx.update(|cx| {
 766                                    let position = buffer.read(cx).anchor_before(match_range.end);
 767                                    tool.set_agent_location(buffer.downgrade(), position, cx);
 768                                });
 769                            }
 770                        }
 771                    }
 772                }
 773
 774                ToolEditEvent::OldTextChunk {
 775                    edit_index,
 776                    chunk,
 777                    done: true,
 778                } => {
 779                    pipeline.ensure_resolving_old_text(*edit_index, buffer, cx);
 780
 781                    let EditPipelineEntry::ResolvingOldText { matcher } =
 782                        &mut pipeline.edits[*edit_index]
 783                    else {
 784                        continue;
 785                    };
 786
 787                    if !chunk.is_empty() {
 788                        matcher.push(chunk, None);
 789                    }
 790                    let matches = matcher.finish();
 791
 792                    if matches.is_empty() {
 793                        return Err(StreamingEditFileToolOutput::error(format!(
 794                            "Could not find matching text for edit at index {}. \
 795                                 The old_text did not match any content in the file. \
 796                                 Please read the file again to get the current content.",
 797                            edit_index,
 798                        )));
 799                    }
 800                    if matches.len() > 1 {
 801                        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 802                        let lines = matches
 803                            .iter()
 804                            .map(|r| (snapshot.offset_to_point(r.start).row + 1).to_string())
 805                            .collect::<Vec<_>>()
 806                            .join(", ");
 807                        return Err(StreamingEditFileToolOutput::error(format!(
 808                            "Edit {} matched multiple locations in the file at lines: {}. \
 809                                 Please provide more context in old_text to uniquely \
 810                                 identify the location.",
 811                            edit_index, lines
 812                        )));
 813                    }
 814
 815                    let range = matches.into_iter().next().expect("checked len above");
 816
 817                    let anchor_range = buffer
 818                        .read_with(cx, |buffer, _cx| buffer.anchor_range_between(range.clone()));
 819                    diff.update(cx, |diff, cx| diff.reveal_range(anchor_range, cx));
 820
 821                    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 822
 823                    let line = snapshot.offset_to_point(range.start).row;
 824                    event_stream.update_fields(
 825                        ToolCallUpdateFields::new()
 826                            .locations(vec![ToolCallLocation::new(abs_path).line(Some(line))]),
 827                    );
 828
 829                    let EditPipelineEntry::ResolvingOldText { matcher } =
 830                        &pipeline.edits[*edit_index]
 831                    else {
 832                        continue;
 833                    };
 834                    let buffer_indent =
 835                        snapshot.line_indent_for_row(snapshot.offset_to_point(range.start).row);
 836                    let query_indent = query_first_line_indent(matcher.query_lines());
 837                    let indent_delta = compute_indent_delta(buffer_indent, query_indent);
 838
 839                    let old_text_in_buffer =
 840                        snapshot.text_for_range(range.clone()).collect::<String>();
 841
 842                    let text_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());
 843                    pipeline.edits[*edit_index] = EditPipelineEntry::StreamingNewText {
 844                        streaming_diff: StreamingDiff::new(old_text_in_buffer),
 845                        edit_cursor: range.start,
 846                        reindenter: Reindenter::new(indent_delta),
 847                        original_snapshot: text_snapshot,
 848                    };
 849
 850                    cx.update(|cx| {
 851                        let position = buffer.read(cx).anchor_before(range.end);
 852                        tool.set_agent_location(buffer.downgrade(), position, cx);
 853                    });
 854                }
 855
 856                ToolEditEvent::NewTextChunk {
 857                    edit_index,
 858                    chunk,
 859                    done: false,
 860                } => {
 861                    if *edit_index >= pipeline.edits.len() {
 862                        continue;
 863                    }
 864                    let EditPipelineEntry::StreamingNewText {
 865                        streaming_diff,
 866                        edit_cursor,
 867                        reindenter,
 868                        original_snapshot,
 869                        ..
 870                    } = &mut pipeline.edits[*edit_index]
 871                    else {
 872                        continue;
 873                    };
 874
 875                    let reindented = reindenter.push(chunk);
 876                    if reindented.is_empty() {
 877                        continue;
 878                    }
 879
 880                    let char_ops = streaming_diff.push_new(&reindented);
 881                    Self::apply_char_operations(
 882                        &char_ops,
 883                        buffer,
 884                        original_snapshot,
 885                        edit_cursor,
 886                        action_log.as_ref(),
 887                        cx,
 888                    );
 889
 890                    let position = original_snapshot.anchor_before(*edit_cursor);
 891                    cx.update(|cx| {
 892                        tool.set_agent_location(buffer.downgrade(), position, cx);
 893                    });
 894                }
 895
 896                ToolEditEvent::NewTextChunk {
 897                    edit_index,
 898                    chunk,
 899                    done: true,
 900                } => {
 901                    if *edit_index >= pipeline.edits.len() {
 902                        continue;
 903                    }
 904
 905                    let EditPipelineEntry::StreamingNewText {
 906                        mut streaming_diff,
 907                        mut edit_cursor,
 908                        mut reindenter,
 909                        original_snapshot,
 910                    } = std::mem::replace(
 911                        &mut pipeline.edits[*edit_index],
 912                        EditPipelineEntry::Done,
 913                    )
 914                    else {
 915                        continue;
 916                    };
 917
 918                    // Flush any remaining reindent buffer + final chunk.
 919                    let mut final_text = reindenter.push(chunk);
 920                    final_text.push_str(&reindenter.finish());
 921
 922                    if !final_text.is_empty() {
 923                        let char_ops = streaming_diff.push_new(&final_text);
 924                        Self::apply_char_operations(
 925                            &char_ops,
 926                            buffer,
 927                            &original_snapshot,
 928                            &mut edit_cursor,
 929                            action_log.as_ref(),
 930                            cx,
 931                        );
 932                    }
 933
 934                    let remaining_ops = streaming_diff.finish();
 935                    Self::apply_char_operations(
 936                        &remaining_ops,
 937                        buffer,
 938                        &original_snapshot,
 939                        &mut edit_cursor,
 940                        action_log.as_ref(),
 941                        cx,
 942                    );
 943
 944                    let position = original_snapshot.anchor_before(edit_cursor);
 945                    cx.update(|cx| {
 946                        tool.set_agent_location(buffer.downgrade(), position, cx);
 947                    });
 948                }
 949            }
 950        }
 951        Ok(())
 952    }
 953
 954    fn apply_char_operations(
 955        ops: &[CharOperation],
 956        buffer: &Entity<Buffer>,
 957        snapshot: &text::BufferSnapshot,
 958        edit_cursor: &mut usize,
 959        action_log: Option<&Entity<ActionLog>>,
 960        cx: &mut AsyncApp,
 961    ) {
 962        for op in ops {
 963            match op {
 964                CharOperation::Insert { text } => {
 965                    let anchor = snapshot.anchor_after(*edit_cursor);
 966                    agent_edit_buffer(&buffer, [(anchor..anchor, text.as_str())], action_log, cx);
 967                }
 968                CharOperation::Delete { bytes } => {
 969                    let delete_end = *edit_cursor + bytes;
 970                    let anchor_range = snapshot.anchor_range_around(*edit_cursor..delete_end);
 971                    agent_edit_buffer(&buffer, [(anchor_range, "")], action_log, cx);
 972                    *edit_cursor = delete_end;
 973                }
 974                CharOperation::Keep { bytes } => {
 975                    *edit_cursor += bytes;
 976                }
 977            }
 978        }
 979    }
 980}
 981
 982/// Edits a buffer and reports the edit to the action log in the same effect
 983/// cycle. This ensures the action log's subscription handler sees the version
 984/// already updated by `buffer_edited`, so it does not misattribute the agent's
 985/// edit as a user edit.
 986fn agent_edit_buffer<I, S, T>(
 987    buffer: &Entity<Buffer>,
 988    edits: I,
 989    action_log: Option<&Entity<ActionLog>>,
 990    cx: &mut AsyncApp,
 991) where
 992    I: IntoIterator<Item = (Range<S>, T)>,
 993    S: ToOffset,
 994    T: Into<Arc<str>>,
 995{
 996    cx.update(|cx| {
 997        buffer.update(cx, |buffer, cx| {
 998            buffer.edit(edits, None, cx);
 999        });
1000        if let Some(action_log) = action_log {
1001            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1002        }
1003    });
1004}
1005
1006fn ensure_buffer_saved(
1007    buffer: &Entity<Buffer>,
1008    abs_path: &PathBuf,
1009    tool: &StreamingEditFileTool,
1010    action_log: Option<&Entity<ActionLog>>,
1011    cx: &mut AsyncApp,
1012) -> Result<(), StreamingEditFileToolOutput> {
1013    let last_read_mtime =
1014        action_log.and_then(|log| log.read_with(cx, |log, _| log.file_read_time(abs_path)));
1015    let check_result = tool.thread.read_with(cx, |thread, cx| {
1016        let current = buffer
1017            .read(cx)
1018            .file()
1019            .and_then(|file| file.disk_state().mtime());
1020        let dirty = buffer.read(cx).is_dirty();
1021        let has_save = thread.has_tool(SaveFileTool::NAME);
1022        let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME);
1023        (current, dirty, has_save, has_restore)
1024    });
1025
1026    let Ok((current_mtime, is_dirty, has_save_tool, has_restore_tool)) = check_result else {
1027        return Ok(());
1028    };
1029
1030    if is_dirty {
1031        let message = match (has_save_tool, has_restore_tool) {
1032            (true, true) => {
1033                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1034                         If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
1035                         If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."
1036            }
1037            (true, false) => {
1038                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1039                         If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
1040                         If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed."
1041            }
1042            (false, true) => {
1043                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1044                         If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \
1045                         If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."
1046            }
1047            (false, false) => {
1048                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \
1049                         then ask them to save or revert the file manually and inform you when it's ok to proceed."
1050            }
1051        };
1052        return Err(StreamingEditFileToolOutput::error(message));
1053    }
1054
1055    if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
1056        if current != last_read {
1057            return Err(StreamingEditFileToolOutput::error(
1058                "The file has been modified since you last read it. \
1059                             Please read the file again to get the current state before editing it.",
1060            ));
1061        }
1062    }
1063
1064    Ok(())
1065}
1066
1067fn resolve_path(
1068    mode: StreamingEditFileMode,
1069    path: &PathBuf,
1070    project: &Entity<Project>,
1071    cx: &mut App,
1072) -> Result<ProjectPath> {
1073    let project = project.read(cx);
1074
1075    match mode {
1076        StreamingEditFileMode::Edit => {
1077            let path = project
1078                .find_project_path(&path, cx)
1079                .context("Can't edit file: path not found")?;
1080
1081            let entry = project
1082                .entry_for_path(&path, cx)
1083                .context("Can't edit file: path not found")?;
1084
1085            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
1086            Ok(path)
1087        }
1088        StreamingEditFileMode::Write => {
1089            if let Some(path) = project.find_project_path(&path, cx)
1090                && let Some(entry) = project.entry_for_path(&path, cx)
1091            {
1092                anyhow::ensure!(entry.is_file(), "Can't write to file: path is a directory");
1093                return Ok(path);
1094            }
1095
1096            let parent_path = path.parent().context("Can't create file: incorrect path")?;
1097
1098            let parent_project_path = project.find_project_path(&parent_path, cx);
1099
1100            let parent_entry = parent_project_path
1101                .as_ref()
1102                .and_then(|path| project.entry_for_path(path, cx))
1103                .context("Can't create file: parent directory doesn't exist")?;
1104
1105            anyhow::ensure!(
1106                parent_entry.is_dir(),
1107                "Can't create file: parent is not a directory"
1108            );
1109
1110            let file_name = path
1111                .file_name()
1112                .and_then(|file_name| file_name.to_str())
1113                .and_then(|file_name| RelPath::unix(file_name).ok())
1114                .context("Can't create file: invalid filename")?;
1115
1116            let new_file_path = parent_project_path.map(|parent| ProjectPath {
1117                path: parent.path.join(file_name),
1118                ..parent
1119            });
1120
1121            new_file_path.context("Can't create file")
1122        }
1123    }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129    use crate::{ContextServerRegistry, Templates, ToolInputSender};
1130    use fs::Fs as _;
1131    use futures::StreamExt as _;
1132    use gpui::{TestAppContext, UpdateGlobal};
1133    use language_model::fake_provider::FakeLanguageModel;
1134    use prompt_store::ProjectContext;
1135    use serde_json::json;
1136    use settings::Settings;
1137    use settings::SettingsStore;
1138    use util::path;
1139    use util::rel_path::rel_path;
1140
1141    #[gpui::test]
1142    async fn test_streaming_edit_create_file(cx: &mut TestAppContext) {
1143        init_test(cx);
1144
1145        let fs = project::FakeFs::new(cx.executor());
1146        fs.insert_tree("/root", json!({"dir": {}})).await;
1147        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1148        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1149        let context_server_registry =
1150            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1151        let model = Arc::new(FakeLanguageModel::default());
1152        let thread = cx.new(|cx| {
1153            crate::Thread::new(
1154                project.clone(),
1155                cx.new(|_cx| ProjectContext::default()),
1156                context_server_registry,
1157                Templates::new(),
1158                Some(model),
1159                cx,
1160            )
1161        });
1162
1163        let result = cx
1164            .update(|cx| {
1165                let input = StreamingEditFileToolInput {
1166                    display_description: "Create new file".into(),
1167                    path: "root/dir/new_file.txt".into(),
1168                    mode: StreamingEditFileMode::Write,
1169                    content: Some("Hello, World!".into()),
1170                    edits: None,
1171                };
1172                Arc::new(StreamingEditFileTool::new(
1173                    project.clone(),
1174                    thread.downgrade(),
1175                    language_registry,
1176                ))
1177                .run(
1178                    ToolInput::resolved(input),
1179                    ToolCallEventStream::test().0,
1180                    cx,
1181                )
1182            })
1183            .await;
1184
1185        let StreamingEditFileToolOutput::Success { new_text, diff, .. } = result.unwrap() else {
1186            panic!("expected success");
1187        };
1188        assert_eq!(new_text, "Hello, World!");
1189        assert!(!diff.is_empty());
1190    }
1191
1192    #[gpui::test]
1193    async fn test_streaming_edit_overwrite_file(cx: &mut TestAppContext) {
1194        init_test(cx);
1195
1196        let fs = project::FakeFs::new(cx.executor());
1197        fs.insert_tree("/root", json!({"file.txt": "old content"}))
1198            .await;
1199        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1200        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1201        let context_server_registry =
1202            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1203        let model = Arc::new(FakeLanguageModel::default());
1204        let thread = cx.new(|cx| {
1205            crate::Thread::new(
1206                project.clone(),
1207                cx.new(|_cx| ProjectContext::default()),
1208                context_server_registry,
1209                Templates::new(),
1210                Some(model),
1211                cx,
1212            )
1213        });
1214
1215        let result = cx
1216            .update(|cx| {
1217                let input = StreamingEditFileToolInput {
1218                    display_description: "Overwrite file".into(),
1219                    path: "root/file.txt".into(),
1220                    mode: StreamingEditFileMode::Write,
1221                    content: Some("new content".into()),
1222                    edits: None,
1223                };
1224                Arc::new(StreamingEditFileTool::new(
1225                    project.clone(),
1226                    thread.downgrade(),
1227                    language_registry,
1228                ))
1229                .run(
1230                    ToolInput::resolved(input),
1231                    ToolCallEventStream::test().0,
1232                    cx,
1233                )
1234            })
1235            .await;
1236
1237        let StreamingEditFileToolOutput::Success {
1238            new_text, old_text, ..
1239        } = result.unwrap()
1240        else {
1241            panic!("expected success");
1242        };
1243        assert_eq!(new_text, "new content");
1244        assert_eq!(*old_text, "old content");
1245    }
1246
1247    #[gpui::test]
1248    async fn test_streaming_edit_granular_edits(cx: &mut TestAppContext) {
1249        init_test(cx);
1250
1251        let fs = project::FakeFs::new(cx.executor());
1252        fs.insert_tree(
1253            "/root",
1254            json!({
1255                "file.txt": "line 1\nline 2\nline 3\n"
1256            }),
1257        )
1258        .await;
1259        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1260        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1261        let context_server_registry =
1262            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1263        let model = Arc::new(FakeLanguageModel::default());
1264        let thread = cx.new(|cx| {
1265            crate::Thread::new(
1266                project.clone(),
1267                cx.new(|_cx| ProjectContext::default()),
1268                context_server_registry,
1269                Templates::new(),
1270                Some(model),
1271                cx,
1272            )
1273        });
1274
1275        let result = cx
1276            .update(|cx| {
1277                let input = StreamingEditFileToolInput {
1278                    display_description: "Edit lines".into(),
1279                    path: "root/file.txt".into(),
1280                    mode: StreamingEditFileMode::Edit,
1281                    content: None,
1282                    edits: Some(vec![Edit {
1283                        old_text: "line 2".into(),
1284                        new_text: "modified line 2".into(),
1285                    }]),
1286                };
1287                Arc::new(StreamingEditFileTool::new(
1288                    project.clone(),
1289                    thread.downgrade(),
1290                    language_registry,
1291                ))
1292                .run(
1293                    ToolInput::resolved(input),
1294                    ToolCallEventStream::test().0,
1295                    cx,
1296                )
1297            })
1298            .await;
1299
1300        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1301            panic!("expected success");
1302        };
1303        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
1304    }
1305
1306    #[gpui::test]
1307    async fn test_streaming_edit_multiple_edits(cx: &mut TestAppContext) {
1308        init_test(cx);
1309
1310        let fs = project::FakeFs::new(cx.executor());
1311        fs.insert_tree(
1312            "/root",
1313            json!({
1314                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1315            }),
1316        )
1317        .await;
1318        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1319        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1320        let context_server_registry =
1321            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1322        let model = Arc::new(FakeLanguageModel::default());
1323        let thread = cx.new(|cx| {
1324            crate::Thread::new(
1325                project.clone(),
1326                cx.new(|_cx| ProjectContext::default()),
1327                context_server_registry,
1328                Templates::new(),
1329                Some(model),
1330                cx,
1331            )
1332        });
1333
1334        let result = cx
1335            .update(|cx| {
1336                let input = StreamingEditFileToolInput {
1337                    display_description: "Edit multiple lines".into(),
1338                    path: "root/file.txt".into(),
1339                    mode: StreamingEditFileMode::Edit,
1340                    content: None,
1341                    edits: Some(vec![
1342                        Edit {
1343                            old_text: "line 5".into(),
1344                            new_text: "modified line 5".into(),
1345                        },
1346                        Edit {
1347                            old_text: "line 1".into(),
1348                            new_text: "modified line 1".into(),
1349                        },
1350                    ]),
1351                };
1352                Arc::new(StreamingEditFileTool::new(
1353                    project.clone(),
1354                    thread.downgrade(),
1355                    language_registry,
1356                ))
1357                .run(
1358                    ToolInput::resolved(input),
1359                    ToolCallEventStream::test().0,
1360                    cx,
1361                )
1362            })
1363            .await;
1364
1365        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1366            panic!("expected success");
1367        };
1368        assert_eq!(
1369            new_text,
1370            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1371        );
1372    }
1373
1374    #[gpui::test]
1375    async fn test_streaming_edit_adjacent_edits(cx: &mut TestAppContext) {
1376        init_test(cx);
1377
1378        let fs = project::FakeFs::new(cx.executor());
1379        fs.insert_tree(
1380            "/root",
1381            json!({
1382                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1383            }),
1384        )
1385        .await;
1386        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1387        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1388        let context_server_registry =
1389            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1390        let model = Arc::new(FakeLanguageModel::default());
1391        let thread = cx.new(|cx| {
1392            crate::Thread::new(
1393                project.clone(),
1394                cx.new(|_cx| ProjectContext::default()),
1395                context_server_registry,
1396                Templates::new(),
1397                Some(model),
1398                cx,
1399            )
1400        });
1401
1402        let result = cx
1403            .update(|cx| {
1404                let input = StreamingEditFileToolInput {
1405                    display_description: "Edit adjacent lines".into(),
1406                    path: "root/file.txt".into(),
1407                    mode: StreamingEditFileMode::Edit,
1408                    content: None,
1409                    edits: Some(vec![
1410                        Edit {
1411                            old_text: "line 2".into(),
1412                            new_text: "modified line 2".into(),
1413                        },
1414                        Edit {
1415                            old_text: "line 3".into(),
1416                            new_text: "modified line 3".into(),
1417                        },
1418                    ]),
1419                };
1420                Arc::new(StreamingEditFileTool::new(
1421                    project.clone(),
1422                    thread.downgrade(),
1423                    language_registry,
1424                ))
1425                .run(
1426                    ToolInput::resolved(input),
1427                    ToolCallEventStream::test().0,
1428                    cx,
1429                )
1430            })
1431            .await;
1432
1433        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1434            panic!("expected success");
1435        };
1436        assert_eq!(
1437            new_text,
1438            "line 1\nmodified line 2\nmodified line 3\nline 4\nline 5\n"
1439        );
1440    }
1441
1442    #[gpui::test]
1443    async fn test_streaming_edit_ascending_order_edits(cx: &mut TestAppContext) {
1444        init_test(cx);
1445
1446        let fs = project::FakeFs::new(cx.executor());
1447        fs.insert_tree(
1448            "/root",
1449            json!({
1450                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1451            }),
1452        )
1453        .await;
1454        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1455        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1456        let context_server_registry =
1457            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1458        let model = Arc::new(FakeLanguageModel::default());
1459        let thread = cx.new(|cx| {
1460            crate::Thread::new(
1461                project.clone(),
1462                cx.new(|_cx| ProjectContext::default()),
1463                context_server_registry,
1464                Templates::new(),
1465                Some(model),
1466                cx,
1467            )
1468        });
1469
1470        let result = cx
1471            .update(|cx| {
1472                let input = StreamingEditFileToolInput {
1473                    display_description: "Edit multiple lines in ascending order".into(),
1474                    path: "root/file.txt".into(),
1475                    mode: StreamingEditFileMode::Edit,
1476                    content: None,
1477                    edits: Some(vec![
1478                        Edit {
1479                            old_text: "line 1".into(),
1480                            new_text: "modified line 1".into(),
1481                        },
1482                        Edit {
1483                            old_text: "line 5".into(),
1484                            new_text: "modified line 5".into(),
1485                        },
1486                    ]),
1487                };
1488                Arc::new(StreamingEditFileTool::new(
1489                    project.clone(),
1490                    thread.downgrade(),
1491                    language_registry,
1492                ))
1493                .run(
1494                    ToolInput::resolved(input),
1495                    ToolCallEventStream::test().0,
1496                    cx,
1497                )
1498            })
1499            .await;
1500
1501        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1502            panic!("expected success");
1503        };
1504        assert_eq!(
1505            new_text,
1506            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1507        );
1508    }
1509
1510    #[gpui::test]
1511    async fn test_streaming_edit_nonexistent_file(cx: &mut TestAppContext) {
1512        init_test(cx);
1513
1514        let fs = project::FakeFs::new(cx.executor());
1515        fs.insert_tree("/root", json!({})).await;
1516        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1517        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1518        let context_server_registry =
1519            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1520        let model = Arc::new(FakeLanguageModel::default());
1521        let thread = cx.new(|cx| {
1522            crate::Thread::new(
1523                project.clone(),
1524                cx.new(|_cx| ProjectContext::default()),
1525                context_server_registry,
1526                Templates::new(),
1527                Some(model),
1528                cx,
1529            )
1530        });
1531
1532        let result = cx
1533            .update(|cx| {
1534                let input = StreamingEditFileToolInput {
1535                    display_description: "Some edit".into(),
1536                    path: "root/nonexistent_file.txt".into(),
1537                    mode: StreamingEditFileMode::Edit,
1538                    content: None,
1539                    edits: Some(vec![Edit {
1540                        old_text: "foo".into(),
1541                        new_text: "bar".into(),
1542                    }]),
1543                };
1544                Arc::new(StreamingEditFileTool::new(
1545                    project,
1546                    thread.downgrade(),
1547                    language_registry,
1548                ))
1549                .run(
1550                    ToolInput::resolved(input),
1551                    ToolCallEventStream::test().0,
1552                    cx,
1553                )
1554            })
1555            .await;
1556
1557        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1558            panic!("expected error");
1559        };
1560        assert_eq!(error, "Can't edit file: path not found");
1561    }
1562
1563    #[gpui::test]
1564    async fn test_streaming_edit_failed_match(cx: &mut TestAppContext) {
1565        init_test(cx);
1566
1567        let fs = project::FakeFs::new(cx.executor());
1568        fs.insert_tree("/root", json!({"file.txt": "hello world"}))
1569            .await;
1570        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1571        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1572        let context_server_registry =
1573            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1574        let model = Arc::new(FakeLanguageModel::default());
1575        let thread = cx.new(|cx| {
1576            crate::Thread::new(
1577                project.clone(),
1578                cx.new(|_cx| ProjectContext::default()),
1579                context_server_registry,
1580                Templates::new(),
1581                Some(model),
1582                cx,
1583            )
1584        });
1585
1586        let result = cx
1587            .update(|cx| {
1588                let input = StreamingEditFileToolInput {
1589                    display_description: "Edit file".into(),
1590                    path: "root/file.txt".into(),
1591                    mode: StreamingEditFileMode::Edit,
1592                    content: None,
1593                    edits: Some(vec![Edit {
1594                        old_text: "nonexistent text that is not in the file".into(),
1595                        new_text: "replacement".into(),
1596                    }]),
1597                };
1598                Arc::new(StreamingEditFileTool::new(
1599                    project,
1600                    thread.downgrade(),
1601                    language_registry,
1602                ))
1603                .run(
1604                    ToolInput::resolved(input),
1605                    ToolCallEventStream::test().0,
1606                    cx,
1607                )
1608            })
1609            .await;
1610
1611        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1612            panic!("expected error");
1613        };
1614        assert!(
1615            error.contains("Could not find matching text"),
1616            "Expected error containing 'Could not find matching text' but got: {error}"
1617        );
1618    }
1619
1620    #[gpui::test]
1621    async fn test_streaming_early_buffer_open(cx: &mut TestAppContext) {
1622        init_test(cx);
1623
1624        let fs = project::FakeFs::new(cx.executor());
1625        fs.insert_tree(
1626            "/root",
1627            json!({
1628                "file.txt": "line 1\nline 2\nline 3\n"
1629            }),
1630        )
1631        .await;
1632        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1633        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1634        let context_server_registry =
1635            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1636        let model = Arc::new(FakeLanguageModel::default());
1637        let thread = cx.new(|cx| {
1638            crate::Thread::new(
1639                project.clone(),
1640                cx.new(|_cx| ProjectContext::default()),
1641                context_server_registry,
1642                Templates::new(),
1643                Some(model),
1644                cx,
1645            )
1646        });
1647
1648        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1649        let (event_stream, _receiver) = ToolCallEventStream::test();
1650
1651        let tool = Arc::new(StreamingEditFileTool::new(
1652            project.clone(),
1653            thread.downgrade(),
1654            language_registry,
1655        ));
1656
1657        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1658
1659        // Send partials simulating LLM streaming: description first, then path, then mode
1660        sender.send_partial(json!({"display_description": "Edit lines"}));
1661        cx.run_until_parked();
1662
1663        sender.send_partial(json!({
1664            "display_description": "Edit lines",
1665            "path": "root/file.txt"
1666        }));
1667        cx.run_until_parked();
1668
1669        // Path is NOT yet complete because mode hasn't appeared — no buffer open yet
1670        sender.send_partial(json!({
1671            "display_description": "Edit lines",
1672            "path": "root/file.txt",
1673            "mode": "edit"
1674        }));
1675        cx.run_until_parked();
1676
1677        // Now send the final complete input
1678        sender.send_final(json!({
1679            "display_description": "Edit lines",
1680            "path": "root/file.txt",
1681            "mode": "edit",
1682            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
1683        }));
1684
1685        let result = task.await;
1686        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1687            panic!("expected success");
1688        };
1689        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
1690    }
1691
1692    #[gpui::test]
1693    async fn test_streaming_path_completeness_heuristic(cx: &mut TestAppContext) {
1694        init_test(cx);
1695
1696        let fs = project::FakeFs::new(cx.executor());
1697        fs.insert_tree(
1698            "/root",
1699            json!({
1700                "file.txt": "hello world"
1701            }),
1702        )
1703        .await;
1704        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1705        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1706        let context_server_registry =
1707            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1708        let model = Arc::new(FakeLanguageModel::default());
1709        let thread = cx.new(|cx| {
1710            crate::Thread::new(
1711                project.clone(),
1712                cx.new(|_cx| ProjectContext::default()),
1713                context_server_registry,
1714                Templates::new(),
1715                Some(model),
1716                cx,
1717            )
1718        });
1719
1720        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1721        let (event_stream, _receiver) = ToolCallEventStream::test();
1722
1723        let tool = Arc::new(StreamingEditFileTool::new(
1724            project.clone(),
1725            thread.downgrade(),
1726            language_registry,
1727        ));
1728
1729        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1730
1731        // Send partial with path but NO mode — path should NOT be treated as complete
1732        sender.send_partial(json!({
1733            "display_description": "Overwrite file",
1734            "path": "root/file"
1735        }));
1736        cx.run_until_parked();
1737
1738        // Now the path grows and mode appears
1739        sender.send_partial(json!({
1740            "display_description": "Overwrite file",
1741            "path": "root/file.txt",
1742            "mode": "write"
1743        }));
1744        cx.run_until_parked();
1745
1746        // Send final
1747        sender.send_final(json!({
1748            "display_description": "Overwrite file",
1749            "path": "root/file.txt",
1750            "mode": "write",
1751            "content": "new content"
1752        }));
1753
1754        let result = task.await;
1755        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1756            panic!("expected success");
1757        };
1758        assert_eq!(new_text, "new content");
1759    }
1760
1761    #[gpui::test]
1762    async fn test_streaming_cancellation_during_partials(cx: &mut TestAppContext) {
1763        init_test(cx);
1764
1765        let fs = project::FakeFs::new(cx.executor());
1766        fs.insert_tree(
1767            "/root",
1768            json!({
1769                "file.txt": "hello world"
1770            }),
1771        )
1772        .await;
1773        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1774        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1775        let context_server_registry =
1776            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1777        let model = Arc::new(FakeLanguageModel::default());
1778        let thread = cx.new(|cx| {
1779            crate::Thread::new(
1780                project.clone(),
1781                cx.new(|_cx| ProjectContext::default()),
1782                context_server_registry,
1783                Templates::new(),
1784                Some(model),
1785                cx,
1786            )
1787        });
1788
1789        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1790        let (event_stream, _receiver, mut cancellation_tx) =
1791            ToolCallEventStream::test_with_cancellation();
1792
1793        let tool = Arc::new(StreamingEditFileTool::new(
1794            project.clone(),
1795            thread.downgrade(),
1796            language_registry,
1797        ));
1798
1799        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1800
1801        // Send a partial
1802        sender.send_partial(json!({"display_description": "Edit"}));
1803        cx.run_until_parked();
1804
1805        // Cancel during streaming
1806        ToolCallEventStream::signal_cancellation_with_sender(&mut cancellation_tx);
1807        cx.run_until_parked();
1808
1809        // The sender is still alive so the partial loop should detect cancellation
1810        // We need to drop the sender to also unblock recv() if the loop didn't catch it
1811        drop(sender);
1812
1813        let result = task.await;
1814        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1815            panic!("expected error");
1816        };
1817        assert!(
1818            error.contains("cancelled"),
1819            "Expected cancellation error but got: {error}"
1820        );
1821    }
1822
1823    #[gpui::test]
1824    async fn test_streaming_edit_with_multiple_partials(cx: &mut TestAppContext) {
1825        init_test(cx);
1826
1827        let fs = project::FakeFs::new(cx.executor());
1828        fs.insert_tree(
1829            "/root",
1830            json!({
1831                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1832            }),
1833        )
1834        .await;
1835        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1836        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1837        let context_server_registry =
1838            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1839        let model = Arc::new(FakeLanguageModel::default());
1840        let thread = cx.new(|cx| {
1841            crate::Thread::new(
1842                project.clone(),
1843                cx.new(|_cx| ProjectContext::default()),
1844                context_server_registry,
1845                Templates::new(),
1846                Some(model),
1847                cx,
1848            )
1849        });
1850
1851        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1852        let (event_stream, _receiver) = ToolCallEventStream::test();
1853
1854        let tool = Arc::new(StreamingEditFileTool::new(
1855            project.clone(),
1856            thread.downgrade(),
1857            language_registry,
1858        ));
1859
1860        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1861
1862        // Simulate fine-grained streaming of the JSON
1863        sender.send_partial(json!({"display_description": "Edit multiple"}));
1864        cx.run_until_parked();
1865
1866        sender.send_partial(json!({
1867            "display_description": "Edit multiple lines",
1868            "path": "root/file.txt"
1869        }));
1870        cx.run_until_parked();
1871
1872        sender.send_partial(json!({
1873            "display_description": "Edit multiple lines",
1874            "path": "root/file.txt",
1875            "mode": "edit"
1876        }));
1877        cx.run_until_parked();
1878
1879        sender.send_partial(json!({
1880            "display_description": "Edit multiple lines",
1881            "path": "root/file.txt",
1882            "mode": "edit",
1883            "edits": [{"old_text": "line 1"}]
1884        }));
1885        cx.run_until_parked();
1886
1887        sender.send_partial(json!({
1888            "display_description": "Edit multiple lines",
1889            "path": "root/file.txt",
1890            "mode": "edit",
1891            "edits": [
1892                {"old_text": "line 1", "new_text": "modified line 1"},
1893                {"old_text": "line 5"}
1894            ]
1895        }));
1896        cx.run_until_parked();
1897
1898        // Send final complete input
1899        sender.send_final(json!({
1900            "display_description": "Edit multiple lines",
1901            "path": "root/file.txt",
1902            "mode": "edit",
1903            "edits": [
1904                {"old_text": "line 1", "new_text": "modified line 1"},
1905                {"old_text": "line 5", "new_text": "modified line 5"}
1906            ]
1907        }));
1908
1909        let result = task.await;
1910        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1911            panic!("expected success");
1912        };
1913        assert_eq!(
1914            new_text,
1915            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1916        );
1917    }
1918
1919    #[gpui::test]
1920    async fn test_streaming_create_file_with_partials(cx: &mut TestAppContext) {
1921        init_test(cx);
1922
1923        let fs = project::FakeFs::new(cx.executor());
1924        fs.insert_tree("/root", json!({"dir": {}})).await;
1925        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1926        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1927        let context_server_registry =
1928            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1929        let model = Arc::new(FakeLanguageModel::default());
1930        let thread = cx.new(|cx| {
1931            crate::Thread::new(
1932                project.clone(),
1933                cx.new(|_cx| ProjectContext::default()),
1934                context_server_registry,
1935                Templates::new(),
1936                Some(model),
1937                cx,
1938            )
1939        });
1940
1941        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1942        let (event_stream, _receiver) = ToolCallEventStream::test();
1943
1944        let tool = Arc::new(StreamingEditFileTool::new(
1945            project.clone(),
1946            thread.downgrade(),
1947            language_registry,
1948        ));
1949
1950        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1951
1952        // Stream partials for create mode
1953        sender.send_partial(json!({"display_description": "Create new file"}));
1954        cx.run_until_parked();
1955
1956        sender.send_partial(json!({
1957            "display_description": "Create new file",
1958            "path": "root/dir/new_file.txt",
1959            "mode": "write"
1960        }));
1961        cx.run_until_parked();
1962
1963        sender.send_partial(json!({
1964            "display_description": "Create new file",
1965            "path": "root/dir/new_file.txt",
1966            "mode": "write",
1967            "content": "Hello, "
1968        }));
1969        cx.run_until_parked();
1970
1971        // Final with full content
1972        sender.send_final(json!({
1973            "display_description": "Create new file",
1974            "path": "root/dir/new_file.txt",
1975            "mode": "write",
1976            "content": "Hello, World!"
1977        }));
1978
1979        let result = task.await;
1980        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1981            panic!("expected success");
1982        };
1983        assert_eq!(new_text, "Hello, World!");
1984    }
1985
1986    #[gpui::test]
1987    async fn test_streaming_no_partials_direct_final(cx: &mut TestAppContext) {
1988        init_test(cx);
1989
1990        let fs = project::FakeFs::new(cx.executor());
1991        fs.insert_tree(
1992            "/root",
1993            json!({
1994                "file.txt": "line 1\nline 2\nline 3\n"
1995            }),
1996        )
1997        .await;
1998        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1999        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2000        let context_server_registry =
2001            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2002        let model = Arc::new(FakeLanguageModel::default());
2003        let thread = cx.new(|cx| {
2004            crate::Thread::new(
2005                project.clone(),
2006                cx.new(|_cx| ProjectContext::default()),
2007                context_server_registry,
2008                Templates::new(),
2009                Some(model),
2010                cx,
2011            )
2012        });
2013
2014        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2015        let (event_stream, _receiver) = ToolCallEventStream::test();
2016
2017        let tool = Arc::new(StreamingEditFileTool::new(
2018            project.clone(),
2019            thread.downgrade(),
2020            language_registry,
2021        ));
2022
2023        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2024
2025        // Send final immediately with no partials (simulates non-streaming path)
2026        sender.send_final(json!({
2027            "display_description": "Edit lines",
2028            "path": "root/file.txt",
2029            "mode": "edit",
2030            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2031        }));
2032
2033        let result = task.await;
2034        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2035            panic!("expected success");
2036        };
2037        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
2038    }
2039
2040    #[gpui::test]
2041    async fn test_streaming_incremental_edit_application(cx: &mut TestAppContext) {
2042        init_test(cx);
2043
2044        let fs = project::FakeFs::new(cx.executor());
2045        fs.insert_tree(
2046            "/root",
2047            json!({
2048                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
2049            }),
2050        )
2051        .await;
2052        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2053        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2054        let context_server_registry =
2055            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2056        let model = Arc::new(FakeLanguageModel::default());
2057        let thread = cx.new(|cx| {
2058            crate::Thread::new(
2059                project.clone(),
2060                cx.new(|_cx| ProjectContext::default()),
2061                context_server_registry,
2062                Templates::new(),
2063                Some(model),
2064                cx,
2065            )
2066        });
2067
2068        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2069        let (event_stream, _receiver) = ToolCallEventStream::test();
2070
2071        let tool = Arc::new(StreamingEditFileTool::new(
2072            project.clone(),
2073            thread.downgrade(),
2074            language_registry,
2075        ));
2076
2077        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2078
2079        // Stream description, path, mode
2080        sender.send_partial(json!({"display_description": "Edit multiple lines"}));
2081        cx.run_until_parked();
2082
2083        sender.send_partial(json!({
2084            "display_description": "Edit multiple lines",
2085            "path": "root/file.txt",
2086            "mode": "edit"
2087        }));
2088        cx.run_until_parked();
2089
2090        // First edit starts streaming (old_text only, still in progress)
2091        sender.send_partial(json!({
2092            "display_description": "Edit multiple lines",
2093            "path": "root/file.txt",
2094            "mode": "edit",
2095            "edits": [{"old_text": "line 1"}]
2096        }));
2097        cx.run_until_parked();
2098
2099        // Buffer should not have changed yet — the first edit is still in progress
2100        // (no second edit has appeared to prove the first is complete)
2101        let buffer_text = project.update(cx, |project, cx| {
2102            let project_path = project.find_project_path(&PathBuf::from("root/file.txt"), cx);
2103            project_path.and_then(|pp| {
2104                project
2105                    .get_open_buffer(&pp, cx)
2106                    .map(|buffer| buffer.read(cx).text())
2107            })
2108        });
2109        // Buffer is open (from streaming) but edit 1 is still in-progress
2110        assert_eq!(
2111            buffer_text.as_deref(),
2112            Some("line 1\nline 2\nline 3\nline 4\nline 5\n"),
2113            "Buffer should not be modified while first edit is still in progress"
2114        );
2115
2116        // Second edit appears — this proves the first edit is complete, so it
2117        // should be applied immediately during streaming
2118        sender.send_partial(json!({
2119            "display_description": "Edit multiple lines",
2120            "path": "root/file.txt",
2121            "mode": "edit",
2122            "edits": [
2123                {"old_text": "line 1", "new_text": "MODIFIED 1"},
2124                {"old_text": "line 5"}
2125            ]
2126        }));
2127        cx.run_until_parked();
2128
2129        // First edit should now be applied to the buffer
2130        let buffer_text = project.update(cx, |project, cx| {
2131            let project_path = project.find_project_path(&PathBuf::from("root/file.txt"), cx);
2132            project_path.and_then(|pp| {
2133                project
2134                    .get_open_buffer(&pp, cx)
2135                    .map(|buffer| buffer.read(cx).text())
2136            })
2137        });
2138        assert_eq!(
2139            buffer_text.as_deref(),
2140            Some("MODIFIED 1\nline 2\nline 3\nline 4\nline 5\n"),
2141            "First edit should be applied during streaming when second edit appears"
2142        );
2143
2144        // Send final complete input
2145        sender.send_final(json!({
2146            "display_description": "Edit multiple lines",
2147            "path": "root/file.txt",
2148            "mode": "edit",
2149            "edits": [
2150                {"old_text": "line 1", "new_text": "MODIFIED 1"},
2151                {"old_text": "line 5", "new_text": "MODIFIED 5"}
2152            ]
2153        }));
2154
2155        let result = task.await;
2156        let StreamingEditFileToolOutput::Success {
2157            new_text, old_text, ..
2158        } = result.unwrap()
2159        else {
2160            panic!("expected success");
2161        };
2162        assert_eq!(new_text, "MODIFIED 1\nline 2\nline 3\nline 4\nMODIFIED 5\n");
2163        assert_eq!(
2164            *old_text, "line 1\nline 2\nline 3\nline 4\nline 5\n",
2165            "old_text should reflect the original file content before any edits"
2166        );
2167    }
2168
2169    #[gpui::test]
2170    async fn test_streaming_incremental_three_edits(cx: &mut TestAppContext) {
2171        init_test(cx);
2172
2173        let fs = project::FakeFs::new(cx.executor());
2174        fs.insert_tree(
2175            "/root",
2176            json!({
2177                "file.txt": "aaa\nbbb\nccc\nddd\neee\n"
2178            }),
2179        )
2180        .await;
2181        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2182        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2183        let context_server_registry =
2184            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2185        let model = Arc::new(FakeLanguageModel::default());
2186        let thread = cx.new(|cx| {
2187            crate::Thread::new(
2188                project.clone(),
2189                cx.new(|_cx| ProjectContext::default()),
2190                context_server_registry,
2191                Templates::new(),
2192                Some(model),
2193                cx,
2194            )
2195        });
2196
2197        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2198        let (event_stream, _receiver) = ToolCallEventStream::test();
2199
2200        let tool = Arc::new(StreamingEditFileTool::new(
2201            project.clone(),
2202            thread.downgrade(),
2203            language_registry,
2204        ));
2205
2206        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2207
2208        // Setup: description + path + mode
2209        sender.send_partial(json!({
2210            "display_description": "Edit three lines",
2211            "path": "root/file.txt",
2212            "mode": "edit"
2213        }));
2214        cx.run_until_parked();
2215
2216        // Edit 1 in progress
2217        sender.send_partial(json!({
2218            "display_description": "Edit three lines",
2219            "path": "root/file.txt",
2220            "mode": "edit",
2221            "edits": [{"old_text": "aaa", "new_text": "AAA"}]
2222        }));
2223        cx.run_until_parked();
2224
2225        // Edit 2 appears — edit 1 is now complete and should be applied
2226        sender.send_partial(json!({
2227            "display_description": "Edit three lines",
2228            "path": "root/file.txt",
2229            "mode": "edit",
2230            "edits": [
2231                {"old_text": "aaa", "new_text": "AAA"},
2232                {"old_text": "ccc", "new_text": "CCC"}
2233            ]
2234        }));
2235        cx.run_until_parked();
2236
2237        // Verify edit 1 fully applied. Edit 2's new_text is being
2238        // streamed: "CCC" is inserted but the old "ccc" isn't deleted
2239        // yet (StreamingDiff::finish runs when edit 3 marks edit 2 done).
2240        let buffer_text = project.update(cx, |project, cx| {
2241            let pp = project
2242                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2243                .unwrap();
2244            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2245        });
2246        assert_eq!(buffer_text.as_deref(), Some("AAA\nbbb\nCCCccc\nddd\neee\n"));
2247
2248        // Edit 3 appears — edit 2 is now complete and should be applied
2249        sender.send_partial(json!({
2250            "display_description": "Edit three lines",
2251            "path": "root/file.txt",
2252            "mode": "edit",
2253            "edits": [
2254                {"old_text": "aaa", "new_text": "AAA"},
2255                {"old_text": "ccc", "new_text": "CCC"},
2256                {"old_text": "eee", "new_text": "EEE"}
2257            ]
2258        }));
2259        cx.run_until_parked();
2260
2261        // Verify edits 1 and 2 fully applied. Edit 3's new_text is being
2262        // streamed: "EEE" is inserted but old "eee" isn't deleted yet.
2263        let buffer_text = project.update(cx, |project, cx| {
2264            let pp = project
2265                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2266                .unwrap();
2267            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2268        });
2269        assert_eq!(buffer_text.as_deref(), Some("AAA\nbbb\nCCC\nddd\nEEEeee\n"));
2270
2271        // Send final
2272        sender.send_final(json!({
2273            "display_description": "Edit three lines",
2274            "path": "root/file.txt",
2275            "mode": "edit",
2276            "edits": [
2277                {"old_text": "aaa", "new_text": "AAA"},
2278                {"old_text": "ccc", "new_text": "CCC"},
2279                {"old_text": "eee", "new_text": "EEE"}
2280            ]
2281        }));
2282
2283        let result = task.await;
2284        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2285            panic!("expected success");
2286        };
2287        assert_eq!(new_text, "AAA\nbbb\nCCC\nddd\nEEE\n");
2288    }
2289
2290    #[gpui::test]
2291    async fn test_streaming_edit_failure_mid_stream(cx: &mut TestAppContext) {
2292        init_test(cx);
2293
2294        let fs = project::FakeFs::new(cx.executor());
2295        fs.insert_tree(
2296            "/root",
2297            json!({
2298                "file.txt": "line 1\nline 2\nline 3\n"
2299            }),
2300        )
2301        .await;
2302        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2303        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2304        let context_server_registry =
2305            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2306        let model = Arc::new(FakeLanguageModel::default());
2307        let thread = cx.new(|cx| {
2308            crate::Thread::new(
2309                project.clone(),
2310                cx.new(|_cx| ProjectContext::default()),
2311                context_server_registry,
2312                Templates::new(),
2313                Some(model),
2314                cx,
2315            )
2316        });
2317
2318        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2319        let (event_stream, _receiver) = ToolCallEventStream::test();
2320
2321        let tool = Arc::new(StreamingEditFileTool::new(
2322            project.clone(),
2323            thread.downgrade(),
2324            language_registry,
2325        ));
2326
2327        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2328
2329        // Setup
2330        sender.send_partial(json!({
2331            "display_description": "Edit lines",
2332            "path": "root/file.txt",
2333            "mode": "edit"
2334        }));
2335        cx.run_until_parked();
2336
2337        // Edit 1 (valid) in progress — not yet complete (no second edit)
2338        sender.send_partial(json!({
2339            "display_description": "Edit lines",
2340            "path": "root/file.txt",
2341            "mode": "edit",
2342            "edits": [
2343                {"old_text": "line 1", "new_text": "MODIFIED"}
2344            ]
2345        }));
2346        cx.run_until_parked();
2347
2348        // Edit 2 appears (will fail to match) — this makes edit 1 complete.
2349        // Edit 1 should be applied. Edit 2 is still in-progress (last edit).
2350        sender.send_partial(json!({
2351            "display_description": "Edit lines",
2352            "path": "root/file.txt",
2353            "mode": "edit",
2354            "edits": [
2355                {"old_text": "line 1", "new_text": "MODIFIED"},
2356                {"old_text": "nonexistent text that does not appear anywhere in the file at all", "new_text": "whatever"}
2357            ]
2358        }));
2359        cx.run_until_parked();
2360
2361        // Verify edit 1 was applied
2362        let buffer_text = project.update(cx, |project, cx| {
2363            let pp = project
2364                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2365                .unwrap();
2366            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2367        });
2368        assert_eq!(
2369            buffer_text.as_deref(),
2370            Some("MODIFIED\nline 2\nline 3\n"),
2371            "First edit should be applied even though second edit will fail"
2372        );
2373
2374        // Edit 3 appears — this makes edit 2 "complete", triggering its
2375        // resolution which should fail (old_text doesn't exist in the file).
2376        sender.send_partial(json!({
2377            "display_description": "Edit lines",
2378            "path": "root/file.txt",
2379            "mode": "edit",
2380            "edits": [
2381                {"old_text": "line 1", "new_text": "MODIFIED"},
2382                {"old_text": "nonexistent text that does not appear anywhere in the file at all", "new_text": "whatever"},
2383                {"old_text": "line 3", "new_text": "MODIFIED 3"}
2384            ]
2385        }));
2386        cx.run_until_parked();
2387
2388        // The error from edit 2 should have propagated out of the partial loop.
2389        // Drop sender to unblock recv() if the loop didn't catch it.
2390        drop(sender);
2391
2392        let result = task.await;
2393        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
2394            panic!("expected error");
2395        };
2396        assert!(
2397            error.contains("Could not find matching text for edit at index 1"),
2398            "Expected error about edit 1 failing, got: {error}"
2399        );
2400    }
2401
2402    #[gpui::test]
2403    async fn test_streaming_single_edit_no_incremental(cx: &mut TestAppContext) {
2404        init_test(cx);
2405
2406        let fs = project::FakeFs::new(cx.executor());
2407        fs.insert_tree(
2408            "/root",
2409            json!({
2410                "file.txt": "hello world\n"
2411            }),
2412        )
2413        .await;
2414        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2415        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2416        let context_server_registry =
2417            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2418        let model = Arc::new(FakeLanguageModel::default());
2419        let thread = cx.new(|cx| {
2420            crate::Thread::new(
2421                project.clone(),
2422                cx.new(|_cx| ProjectContext::default()),
2423                context_server_registry,
2424                Templates::new(),
2425                Some(model),
2426                cx,
2427            )
2428        });
2429
2430        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2431        let (event_stream, _receiver) = ToolCallEventStream::test();
2432
2433        let tool = Arc::new(StreamingEditFileTool::new(
2434            project.clone(),
2435            thread.downgrade(),
2436            language_registry,
2437        ));
2438
2439        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2440
2441        // Setup + single edit that stays in-progress (no second edit to prove completion)
2442        sender.send_partial(json!({
2443            "display_description": "Single edit",
2444            "path": "root/file.txt",
2445            "mode": "edit",
2446            "edits": [{"old_text": "hello world", "new_text": "goodbye world"}]
2447        }));
2448        cx.run_until_parked();
2449
2450        // The edit's old_text and new_text both arrived in one partial, so
2451        // the old_text is resolved and new_text is being streamed via
2452        // StreamingDiff. The buffer reflects the in-progress diff (new text
2453        // inserted, old text not yet fully removed until finalization).
2454        let buffer_text = project.update(cx, |project, cx| {
2455            let pp = project
2456                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2457                .unwrap();
2458            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2459        });
2460        assert_eq!(
2461            buffer_text.as_deref(),
2462            Some("goodbye worldhello world\n"),
2463            "In-progress streaming diff: new text inserted, old text not yet removed"
2464        );
2465
2466        // Send final — the edit is applied during finalization
2467        sender.send_final(json!({
2468            "display_description": "Single edit",
2469            "path": "root/file.txt",
2470            "mode": "edit",
2471            "edits": [{"old_text": "hello world", "new_text": "goodbye world"}]
2472        }));
2473
2474        let result = task.await;
2475        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2476            panic!("expected success");
2477        };
2478        assert_eq!(new_text, "goodbye world\n");
2479    }
2480
2481    #[gpui::test]
2482    async fn test_streaming_input_partials_then_final(cx: &mut TestAppContext) {
2483        init_test(cx);
2484
2485        let fs = project::FakeFs::new(cx.executor());
2486        fs.insert_tree(
2487            "/root",
2488            json!({
2489                "file.txt": "line 1\nline 2\nline 3\n"
2490            }),
2491        )
2492        .await;
2493        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2494        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2495        let context_server_registry =
2496            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2497        let model = Arc::new(FakeLanguageModel::default());
2498        let thread = cx.new(|cx| {
2499            crate::Thread::new(
2500                project.clone(),
2501                cx.new(|_cx| ProjectContext::default()),
2502                context_server_registry,
2503                Templates::new(),
2504                Some(model),
2505                cx,
2506            )
2507        });
2508
2509        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2510            ToolInput::test();
2511
2512        let (event_stream, _event_rx) = ToolCallEventStream::test();
2513        let task = cx.update(|cx| {
2514            Arc::new(StreamingEditFileTool::new(
2515                project.clone(),
2516                thread.downgrade(),
2517                language_registry,
2518            ))
2519            .run(input, event_stream, cx)
2520        });
2521
2522        // Send progressively more complete partial snapshots, as the LLM would
2523        sender.send_partial(json!({
2524            "display_description": "Edit lines"
2525        }));
2526        cx.run_until_parked();
2527
2528        sender.send_partial(json!({
2529            "display_description": "Edit lines",
2530            "path": "root/file.txt",
2531            "mode": "edit"
2532        }));
2533        cx.run_until_parked();
2534
2535        sender.send_partial(json!({
2536            "display_description": "Edit lines",
2537            "path": "root/file.txt",
2538            "mode": "edit",
2539            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2540        }));
2541        cx.run_until_parked();
2542
2543        // Send the final complete input
2544        sender.send_final(json!({
2545            "display_description": "Edit lines",
2546            "path": "root/file.txt",
2547            "mode": "edit",
2548            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2549        }));
2550
2551        let result = task.await;
2552        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2553            panic!("expected success");
2554        };
2555        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
2556    }
2557
2558    #[gpui::test]
2559    async fn test_streaming_input_sender_dropped_before_final(cx: &mut TestAppContext) {
2560        init_test(cx);
2561
2562        let fs = project::FakeFs::new(cx.executor());
2563        fs.insert_tree(
2564            "/root",
2565            json!({
2566                "file.txt": "hello world\n"
2567            }),
2568        )
2569        .await;
2570        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2571        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2572        let context_server_registry =
2573            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2574        let model = Arc::new(FakeLanguageModel::default());
2575        let thread = cx.new(|cx| {
2576            crate::Thread::new(
2577                project.clone(),
2578                cx.new(|_cx| ProjectContext::default()),
2579                context_server_registry,
2580                Templates::new(),
2581                Some(model),
2582                cx,
2583            )
2584        });
2585
2586        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2587            ToolInput::test();
2588
2589        let (event_stream, _event_rx) = ToolCallEventStream::test();
2590        let task = cx.update(|cx| {
2591            Arc::new(StreamingEditFileTool::new(
2592                project.clone(),
2593                thread.downgrade(),
2594                language_registry,
2595            ))
2596            .run(input, event_stream, cx)
2597        });
2598
2599        // Send a partial then drop the sender without sending final
2600        sender.send_partial(json!({
2601            "display_description": "Edit file"
2602        }));
2603        cx.run_until_parked();
2604
2605        drop(sender);
2606
2607        let result = task.await;
2608        assert!(
2609            result.is_err(),
2610            "Tool should error when sender is dropped without sending final input"
2611        );
2612    }
2613
2614    #[gpui::test]
2615    async fn test_streaming_input_recv_drains_partials(cx: &mut TestAppContext) {
2616        init_test(cx);
2617
2618        let fs = project::FakeFs::new(cx.executor());
2619        fs.insert_tree("/root", json!({"dir": {}})).await;
2620        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2621        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2622        let context_server_registry =
2623            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2624        let model = Arc::new(FakeLanguageModel::default());
2625        let thread = cx.new(|cx| {
2626            crate::Thread::new(
2627                project.clone(),
2628                cx.new(|_cx| ProjectContext::default()),
2629                context_server_registry,
2630                Templates::new(),
2631                Some(model),
2632                cx,
2633            )
2634        });
2635
2636        // Create a channel and send multiple partials before a final, then use
2637        // ToolInput::resolved-style immediate delivery to confirm recv() works
2638        // when partials are already buffered.
2639        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2640            ToolInput::test();
2641
2642        let (event_stream, _event_rx) = ToolCallEventStream::test();
2643        let task = cx.update(|cx| {
2644            Arc::new(StreamingEditFileTool::new(
2645                project.clone(),
2646                thread.downgrade(),
2647                language_registry,
2648            ))
2649            .run(input, event_stream, cx)
2650        });
2651
2652        // Buffer several partials before sending the final
2653        sender.send_partial(json!({"display_description": "Create"}));
2654        sender.send_partial(json!({"display_description": "Create", "path": "root/dir/new.txt"}));
2655        sender.send_partial(json!({
2656            "display_description": "Create",
2657            "path": "root/dir/new.txt",
2658            "mode": "write"
2659        }));
2660        sender.send_final(json!({
2661            "display_description": "Create",
2662            "path": "root/dir/new.txt",
2663            "mode": "write",
2664            "content": "streamed content"
2665        }));
2666
2667        let result = task.await;
2668        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2669            panic!("expected success");
2670        };
2671        assert_eq!(new_text, "streamed content");
2672    }
2673
2674    #[gpui::test]
2675    async fn test_streaming_resolve_path_for_creating_file(cx: &mut TestAppContext) {
2676        let mode = StreamingEditFileMode::Write;
2677
2678        let result = test_resolve_path(&mode, "root/new.txt", cx);
2679        assert_resolved_path_eq(result.await, rel_path("new.txt"));
2680
2681        let result = test_resolve_path(&mode, "new.txt", cx);
2682        assert_resolved_path_eq(result.await, rel_path("new.txt"));
2683
2684        let result = test_resolve_path(&mode, "dir/new.txt", cx);
2685        assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
2686
2687        let result = test_resolve_path(&mode, "root/dir/subdir/existing.txt", cx);
2688        assert_resolved_path_eq(result.await, rel_path("dir/subdir/existing.txt"));
2689
2690        let result = test_resolve_path(&mode, "root/dir/subdir", cx);
2691        assert_eq!(
2692            result.await.unwrap_err().to_string(),
2693            "Can't write to file: path is a directory"
2694        );
2695
2696        let result = test_resolve_path(&mode, "root/dir/nonexistent_dir/new.txt", cx);
2697        assert_eq!(
2698            result.await.unwrap_err().to_string(),
2699            "Can't create file: parent directory doesn't exist"
2700        );
2701    }
2702
2703    #[gpui::test]
2704    async fn test_streaming_resolve_path_for_editing_file(cx: &mut TestAppContext) {
2705        let mode = StreamingEditFileMode::Edit;
2706
2707        let path_with_root = "root/dir/subdir/existing.txt";
2708        let path_without_root = "dir/subdir/existing.txt";
2709        let result = test_resolve_path(&mode, path_with_root, cx);
2710        assert_resolved_path_eq(result.await, rel_path(path_without_root));
2711
2712        let result = test_resolve_path(&mode, path_without_root, cx);
2713        assert_resolved_path_eq(result.await, rel_path(path_without_root));
2714
2715        let result = test_resolve_path(&mode, "root/nonexistent.txt", cx);
2716        assert_eq!(
2717            result.await.unwrap_err().to_string(),
2718            "Can't edit file: path not found"
2719        );
2720
2721        let result = test_resolve_path(&mode, "root/dir", cx);
2722        assert_eq!(
2723            result.await.unwrap_err().to_string(),
2724            "Can't edit file: path is a directory"
2725        );
2726    }
2727
2728    async fn test_resolve_path(
2729        mode: &StreamingEditFileMode,
2730        path: &str,
2731        cx: &mut TestAppContext,
2732    ) -> anyhow::Result<ProjectPath> {
2733        init_test(cx);
2734
2735        let fs = project::FakeFs::new(cx.executor());
2736        fs.insert_tree(
2737            "/root",
2738            json!({
2739                "dir": {
2740                    "subdir": {
2741                        "existing.txt": "hello"
2742                    }
2743                }
2744            }),
2745        )
2746        .await;
2747        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2748
2749        cx.update(|cx| resolve_path(mode.clone(), &PathBuf::from(path), &project, cx))
2750    }
2751
2752    #[track_caller]
2753    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
2754        let actual = path.expect("Should return valid path").path;
2755        assert_eq!(actual.as_ref(), expected);
2756    }
2757
2758    #[gpui::test]
2759    async fn test_streaming_format_on_save(cx: &mut TestAppContext) {
2760        init_test(cx);
2761
2762        let fs = project::FakeFs::new(cx.executor());
2763        fs.insert_tree("/root", json!({"src": {}})).await;
2764
2765        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2766
2767        let rust_language = Arc::new(language::Language::new(
2768            language::LanguageConfig {
2769                name: "Rust".into(),
2770                matcher: language::LanguageMatcher {
2771                    path_suffixes: vec!["rs".to_string()],
2772                    ..Default::default()
2773                },
2774                ..Default::default()
2775            },
2776            None,
2777        ));
2778
2779        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2780        language_registry.add(rust_language);
2781
2782        let mut fake_language_servers = language_registry.register_fake_lsp(
2783            "Rust",
2784            language::FakeLspAdapter {
2785                capabilities: lsp::ServerCapabilities {
2786                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
2787                    ..Default::default()
2788                },
2789                ..Default::default()
2790            },
2791        );
2792
2793        fs.save(
2794            path!("/root/src/main.rs").as_ref(),
2795            &"initial content".into(),
2796            language::LineEnding::Unix,
2797        )
2798        .await
2799        .unwrap();
2800
2801        // Open the buffer to trigger LSP initialization
2802        let buffer = project
2803            .update(cx, |project, cx| {
2804                project.open_local_buffer(path!("/root/src/main.rs"), cx)
2805            })
2806            .await
2807            .unwrap();
2808
2809        // Register the buffer with language servers
2810        let _handle = project.update(cx, |project, cx| {
2811            project.register_buffer_with_language_servers(&buffer, cx)
2812        });
2813
2814        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
2815        const FORMATTED_CONTENT: &str =
2816            "This file was formatted by the fake formatter in the test.\n";
2817
2818        // Get the fake language server and set up formatting handler
2819        let fake_language_server = fake_language_servers.next().await.unwrap();
2820        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
2821            |_, _| async move {
2822                Ok(Some(vec![lsp::TextEdit {
2823                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
2824                    new_text: FORMATTED_CONTENT.to_string(),
2825                }]))
2826            }
2827        });
2828
2829        let context_server_registry =
2830            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2831        let model = Arc::new(FakeLanguageModel::default());
2832        let thread = cx.new(|cx| {
2833            crate::Thread::new(
2834                project.clone(),
2835                cx.new(|_cx| ProjectContext::default()),
2836                context_server_registry,
2837                Templates::new(),
2838                Some(model.clone()),
2839                cx,
2840            )
2841        });
2842
2843        // Test with format_on_save enabled
2844        cx.update(|cx| {
2845            SettingsStore::update_global(cx, |store, cx| {
2846                store.update_user_settings(cx, |settings| {
2847                    settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
2848                    settings.project.all_languages.defaults.formatter =
2849                        Some(language::language_settings::FormatterList::default());
2850                });
2851            });
2852        });
2853
2854        // Use streaming pattern so executor can pump the LSP request/response
2855        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2856        let (event_stream, _receiver) = ToolCallEventStream::test();
2857
2858        let tool = Arc::new(StreamingEditFileTool::new(
2859            project.clone(),
2860            thread.downgrade(),
2861            language_registry.clone(),
2862        ));
2863
2864        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2865
2866        sender.send_partial(json!({
2867            "display_description": "Create main function",
2868            "path": "root/src/main.rs",
2869            "mode": "write"
2870        }));
2871        cx.run_until_parked();
2872
2873        sender.send_final(json!({
2874            "display_description": "Create main function",
2875            "path": "root/src/main.rs",
2876            "mode": "write",
2877            "content": UNFORMATTED_CONTENT
2878        }));
2879
2880        let result = task.await;
2881        assert!(result.is_ok());
2882
2883        cx.executor().run_until_parked();
2884
2885        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
2886        assert_eq!(
2887            new_content.replace("\r\n", "\n"),
2888            FORMATTED_CONTENT,
2889            "Code should be formatted when format_on_save is enabled"
2890        );
2891
2892        let stale_buffer_count = thread
2893            .read_with(cx, |thread, _cx| thread.action_log.clone())
2894            .read_with(cx, |log, cx| log.stale_buffers(cx).count());
2895
2896        assert_eq!(
2897            stale_buffer_count, 0,
2898            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers.",
2899            stale_buffer_count
2900        );
2901
2902        // Test with format_on_save disabled
2903        cx.update(|cx| {
2904            SettingsStore::update_global(cx, |store, cx| {
2905                store.update_user_settings(cx, |settings| {
2906                    settings.project.all_languages.defaults.format_on_save =
2907                        Some(FormatOnSave::Off);
2908                });
2909            });
2910        });
2911
2912        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2913        let (event_stream, _receiver) = ToolCallEventStream::test();
2914
2915        let tool = Arc::new(StreamingEditFileTool::new(
2916            project.clone(),
2917            thread.downgrade(),
2918            language_registry,
2919        ));
2920
2921        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2922
2923        sender.send_partial(json!({
2924            "display_description": "Update main function",
2925            "path": "root/src/main.rs",
2926            "mode": "write"
2927        }));
2928        cx.run_until_parked();
2929
2930        sender.send_final(json!({
2931            "display_description": "Update main function",
2932            "path": "root/src/main.rs",
2933            "mode": "write",
2934            "content": UNFORMATTED_CONTENT
2935        }));
2936
2937        let result = task.await;
2938        assert!(result.is_ok());
2939
2940        cx.executor().run_until_parked();
2941
2942        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
2943        assert_eq!(
2944            new_content.replace("\r\n", "\n"),
2945            UNFORMATTED_CONTENT,
2946            "Code should not be formatted when format_on_save is disabled"
2947        );
2948    }
2949
2950    #[gpui::test]
2951    async fn test_streaming_remove_trailing_whitespace(cx: &mut TestAppContext) {
2952        init_test(cx);
2953
2954        let fs = project::FakeFs::new(cx.executor());
2955        fs.insert_tree("/root", json!({"src": {}})).await;
2956
2957        fs.save(
2958            path!("/root/src/main.rs").as_ref(),
2959            &"initial content".into(),
2960            language::LineEnding::Unix,
2961        )
2962        .await
2963        .unwrap();
2964
2965        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2966        let context_server_registry =
2967            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2968        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2969        let model = Arc::new(FakeLanguageModel::default());
2970        let thread = cx.new(|cx| {
2971            crate::Thread::new(
2972                project.clone(),
2973                cx.new(|_cx| ProjectContext::default()),
2974                context_server_registry,
2975                Templates::new(),
2976                Some(model.clone()),
2977                cx,
2978            )
2979        });
2980
2981        // Test with remove_trailing_whitespace_on_save enabled
2982        cx.update(|cx| {
2983            SettingsStore::update_global(cx, |store, cx| {
2984                store.update_user_settings(cx, |settings| {
2985                    settings
2986                        .project
2987                        .all_languages
2988                        .defaults
2989                        .remove_trailing_whitespace_on_save = Some(true);
2990                });
2991            });
2992        });
2993
2994        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
2995            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
2996
2997        let result = cx
2998            .update(|cx| {
2999                let input = StreamingEditFileToolInput {
3000                    display_description: "Create main function".into(),
3001                    path: "root/src/main.rs".into(),
3002                    mode: StreamingEditFileMode::Write,
3003                    content: Some(CONTENT_WITH_TRAILING_WHITESPACE.into()),
3004                    edits: None,
3005                };
3006                Arc::new(StreamingEditFileTool::new(
3007                    project.clone(),
3008                    thread.downgrade(),
3009                    language_registry.clone(),
3010                ))
3011                .run(
3012                    ToolInput::resolved(input),
3013                    ToolCallEventStream::test().0,
3014                    cx,
3015                )
3016            })
3017            .await;
3018        assert!(result.is_ok());
3019
3020        cx.executor().run_until_parked();
3021
3022        assert_eq!(
3023            fs.load(path!("/root/src/main.rs").as_ref())
3024                .await
3025                .unwrap()
3026                .replace("\r\n", "\n"),
3027            "fn main() {\n    println!(\"Hello!\");\n}\n",
3028            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
3029        );
3030
3031        // Test with remove_trailing_whitespace_on_save disabled
3032        cx.update(|cx| {
3033            SettingsStore::update_global(cx, |store, cx| {
3034                store.update_user_settings(cx, |settings| {
3035                    settings
3036                        .project
3037                        .all_languages
3038                        .defaults
3039                        .remove_trailing_whitespace_on_save = Some(false);
3040                });
3041            });
3042        });
3043
3044        let result = cx
3045            .update(|cx| {
3046                let input = StreamingEditFileToolInput {
3047                    display_description: "Update main function".into(),
3048                    path: "root/src/main.rs".into(),
3049                    mode: StreamingEditFileMode::Write,
3050                    content: Some(CONTENT_WITH_TRAILING_WHITESPACE.into()),
3051                    edits: None,
3052                };
3053                Arc::new(StreamingEditFileTool::new(
3054                    project.clone(),
3055                    thread.downgrade(),
3056                    language_registry,
3057                ))
3058                .run(
3059                    ToolInput::resolved(input),
3060                    ToolCallEventStream::test().0,
3061                    cx,
3062                )
3063            })
3064            .await;
3065        assert!(result.is_ok());
3066
3067        cx.executor().run_until_parked();
3068
3069        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
3070        assert_eq!(
3071            final_content.replace("\r\n", "\n"),
3072            CONTENT_WITH_TRAILING_WHITESPACE,
3073            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
3074        );
3075    }
3076
3077    #[gpui::test]
3078    async fn test_streaming_authorize(cx: &mut TestAppContext) {
3079        init_test(cx);
3080        let fs = project::FakeFs::new(cx.executor());
3081        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3082        let context_server_registry =
3083            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3084        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3085        let model = Arc::new(FakeLanguageModel::default());
3086        let thread = cx.new(|cx| {
3087            crate::Thread::new(
3088                project.clone(),
3089                cx.new(|_cx| ProjectContext::default()),
3090                context_server_registry,
3091                Templates::new(),
3092                Some(model.clone()),
3093                cx,
3094            )
3095        });
3096        let tool = Arc::new(StreamingEditFileTool::new(
3097            project.clone(),
3098            thread.downgrade(),
3099            language_registry,
3100        ));
3101        fs.insert_tree("/root", json!({})).await;
3102
3103        // Test 1: Path with .zed component should require confirmation
3104        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3105        let _auth = cx.update(|cx| {
3106            tool.authorize(
3107                &PathBuf::from(".zed/settings.json"),
3108                "test 1",
3109                &stream_tx,
3110                cx,
3111            )
3112        });
3113
3114        let event = stream_rx.expect_authorization().await;
3115        assert_eq!(
3116            event.tool_call.fields.title,
3117            Some("test 1 (local settings)".into())
3118        );
3119
3120        // Test 2: Path outside project should require confirmation
3121        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3122        let _auth =
3123            cx.update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 2", &stream_tx, cx));
3124
3125        let event = stream_rx.expect_authorization().await;
3126        assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
3127
3128        // Test 3: Relative path without .zed should not require confirmation
3129        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3130        cx.update(|cx| {
3131            tool.authorize(&PathBuf::from("root/src/main.rs"), "test 3", &stream_tx, cx)
3132        })
3133        .await
3134        .unwrap();
3135        assert!(stream_rx.try_next().is_err());
3136
3137        // Test 4: Path with .zed in the middle should require confirmation
3138        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3139        let _auth = cx.update(|cx| {
3140            tool.authorize(
3141                &PathBuf::from("root/.zed/tasks.json"),
3142                "test 4",
3143                &stream_tx,
3144                cx,
3145            )
3146        });
3147        let event = stream_rx.expect_authorization().await;
3148        assert_eq!(
3149            event.tool_call.fields.title,
3150            Some("test 4 (local settings)".into())
3151        );
3152
3153        // Test 5: When global default is allow, sensitive and outside-project
3154        // paths still require confirmation
3155        cx.update(|cx| {
3156            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3157            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
3158            agent_settings::AgentSettings::override_global(settings, cx);
3159        });
3160
3161        // 5.1: .zed/settings.json is a sensitive path — still prompts
3162        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3163        let _auth = cx.update(|cx| {
3164            tool.authorize(
3165                &PathBuf::from(".zed/settings.json"),
3166                "test 5.1",
3167                &stream_tx,
3168                cx,
3169            )
3170        });
3171        let event = stream_rx.expect_authorization().await;
3172        assert_eq!(
3173            event.tool_call.fields.title,
3174            Some("test 5.1 (local settings)".into())
3175        );
3176
3177        // 5.2: /etc/hosts is outside the project, but Allow auto-approves
3178        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3179        cx.update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 5.2", &stream_tx, cx))
3180            .await
3181            .unwrap();
3182        assert!(stream_rx.try_next().is_err());
3183
3184        // 5.3: Normal in-project path with allow — no confirmation needed
3185        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3186        cx.update(|cx| {
3187            tool.authorize(
3188                &PathBuf::from("root/src/main.rs"),
3189                "test 5.3",
3190                &stream_tx,
3191                cx,
3192            )
3193        })
3194        .await
3195        .unwrap();
3196        assert!(stream_rx.try_next().is_err());
3197
3198        // 5.4: With Confirm default, non-project paths still prompt
3199        cx.update(|cx| {
3200            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3201            settings.tool_permissions.default = settings::ToolPermissionMode::Confirm;
3202            agent_settings::AgentSettings::override_global(settings, cx);
3203        });
3204
3205        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3206        let _auth = cx
3207            .update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 5.4", &stream_tx, cx));
3208
3209        let event = stream_rx.expect_authorization().await;
3210        assert_eq!(event.tool_call.fields.title, Some("test 5.4".into()));
3211    }
3212
3213    #[gpui::test]
3214    async fn test_streaming_authorize_create_under_symlink_with_allow(cx: &mut TestAppContext) {
3215        init_test(cx);
3216
3217        let fs = project::FakeFs::new(cx.executor());
3218        fs.insert_tree("/root", json!({})).await;
3219        fs.insert_tree("/outside", json!({})).await;
3220        fs.insert_symlink("/root/link", PathBuf::from("/outside"))
3221            .await;
3222
3223        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3224        let context_server_registry =
3225            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3226        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3227        let model = Arc::new(FakeLanguageModel::default());
3228        let thread = cx.new(|cx| {
3229            crate::Thread::new(
3230                project.clone(),
3231                cx.new(|_cx| ProjectContext::default()),
3232                context_server_registry,
3233                Templates::new(),
3234                Some(model),
3235                cx,
3236            )
3237        });
3238        let tool = Arc::new(StreamingEditFileTool::new(
3239            project,
3240            thread.downgrade(),
3241            language_registry,
3242        ));
3243
3244        cx.update(|cx| {
3245            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3246            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
3247            agent_settings::AgentSettings::override_global(settings, cx);
3248        });
3249
3250        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3251        let authorize_task = cx.update(|cx| {
3252            tool.authorize(
3253                &PathBuf::from("link/new.txt"),
3254                "create through symlink",
3255                &stream_tx,
3256                cx,
3257            )
3258        });
3259
3260        let event = stream_rx.expect_authorization().await;
3261        assert!(
3262            event
3263                .tool_call
3264                .fields
3265                .title
3266                .as_deref()
3267                .is_some_and(|title| title.contains("points outside the project")),
3268            "Expected symlink escape authorization for create under external symlink"
3269        );
3270
3271        event
3272            .response
3273            .send(acp::PermissionOptionId::new("allow"))
3274            .unwrap();
3275        authorize_task.await.unwrap();
3276    }
3277
3278    #[gpui::test]
3279    async fn test_streaming_edit_file_symlink_escape_requests_authorization(
3280        cx: &mut TestAppContext,
3281    ) {
3282        init_test(cx);
3283
3284        let fs = project::FakeFs::new(cx.executor());
3285        fs.insert_tree(
3286            path!("/root"),
3287            json!({
3288                "src": { "main.rs": "fn main() {}" }
3289            }),
3290        )
3291        .await;
3292        fs.insert_tree(
3293            path!("/outside"),
3294            json!({
3295                "config.txt": "old content"
3296            }),
3297        )
3298        .await;
3299        fs.create_symlink(
3300            path!("/root/link_to_external").as_ref(),
3301            PathBuf::from("/outside"),
3302        )
3303        .await
3304        .unwrap();
3305
3306        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3307        cx.executor().run_until_parked();
3308
3309        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3310        let context_server_registry =
3311            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3312        let model = Arc::new(FakeLanguageModel::default());
3313        let thread = cx.new(|cx| {
3314            crate::Thread::new(
3315                project.clone(),
3316                cx.new(|_cx| ProjectContext::default()),
3317                context_server_registry,
3318                Templates::new(),
3319                Some(model),
3320                cx,
3321            )
3322        });
3323        let tool = Arc::new(StreamingEditFileTool::new(
3324            project.clone(),
3325            thread.downgrade(),
3326            language_registry,
3327        ));
3328
3329        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3330        let _authorize_task = cx.update(|cx| {
3331            tool.authorize(
3332                &PathBuf::from("link_to_external/config.txt"),
3333                "edit through symlink",
3334                &stream_tx,
3335                cx,
3336            )
3337        });
3338
3339        let auth = stream_rx.expect_authorization().await;
3340        let title = auth.tool_call.fields.title.as_deref().unwrap_or("");
3341        assert!(
3342            title.contains("points outside the project"),
3343            "title should mention symlink escape, got: {title}"
3344        );
3345    }
3346
3347    #[gpui::test]
3348    async fn test_streaming_edit_file_symlink_escape_denied(cx: &mut TestAppContext) {
3349        init_test(cx);
3350
3351        let fs = project::FakeFs::new(cx.executor());
3352        fs.insert_tree(
3353            path!("/root"),
3354            json!({
3355                "src": { "main.rs": "fn main() {}" }
3356            }),
3357        )
3358        .await;
3359        fs.insert_tree(
3360            path!("/outside"),
3361            json!({
3362                "config.txt": "old content"
3363            }),
3364        )
3365        .await;
3366        fs.create_symlink(
3367            path!("/root/link_to_external").as_ref(),
3368            PathBuf::from("/outside"),
3369        )
3370        .await
3371        .unwrap();
3372
3373        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3374        cx.executor().run_until_parked();
3375
3376        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3377        let context_server_registry =
3378            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3379        let model = Arc::new(FakeLanguageModel::default());
3380        let thread = cx.new(|cx| {
3381            crate::Thread::new(
3382                project.clone(),
3383                cx.new(|_cx| ProjectContext::default()),
3384                context_server_registry,
3385                Templates::new(),
3386                Some(model),
3387                cx,
3388            )
3389        });
3390        let tool = Arc::new(StreamingEditFileTool::new(
3391            project.clone(),
3392            thread.downgrade(),
3393            language_registry,
3394        ));
3395
3396        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3397        let authorize_task = cx.update(|cx| {
3398            tool.authorize(
3399                &PathBuf::from("link_to_external/config.txt"),
3400                "edit through symlink",
3401                &stream_tx,
3402                cx,
3403            )
3404        });
3405
3406        let auth = stream_rx.expect_authorization().await;
3407        drop(auth); // deny by dropping
3408
3409        let result = authorize_task.await;
3410        assert!(result.is_err(), "should fail when denied");
3411    }
3412
3413    #[gpui::test]
3414    async fn test_streaming_edit_file_symlink_escape_honors_deny_policy(cx: &mut TestAppContext) {
3415        init_test(cx);
3416        cx.update(|cx| {
3417            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3418            settings.tool_permissions.tools.insert(
3419                "edit_file".into(),
3420                agent_settings::ToolRules {
3421                    default: Some(settings::ToolPermissionMode::Deny),
3422                    ..Default::default()
3423                },
3424            );
3425            agent_settings::AgentSettings::override_global(settings, cx);
3426        });
3427
3428        let fs = project::FakeFs::new(cx.executor());
3429        fs.insert_tree(
3430            path!("/root"),
3431            json!({
3432                "src": { "main.rs": "fn main() {}" }
3433            }),
3434        )
3435        .await;
3436        fs.insert_tree(
3437            path!("/outside"),
3438            json!({
3439                "config.txt": "old content"
3440            }),
3441        )
3442        .await;
3443        fs.create_symlink(
3444            path!("/root/link_to_external").as_ref(),
3445            PathBuf::from("/outside"),
3446        )
3447        .await
3448        .unwrap();
3449
3450        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3451        cx.executor().run_until_parked();
3452
3453        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3454        let context_server_registry =
3455            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3456        let model = Arc::new(FakeLanguageModel::default());
3457        let thread = cx.new(|cx| {
3458            crate::Thread::new(
3459                project.clone(),
3460                cx.new(|_cx| ProjectContext::default()),
3461                context_server_registry,
3462                Templates::new(),
3463                Some(model),
3464                cx,
3465            )
3466        });
3467        let tool = Arc::new(StreamingEditFileTool::new(
3468            project.clone(),
3469            thread.downgrade(),
3470            language_registry,
3471        ));
3472
3473        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3474        let result = cx
3475            .update(|cx| {
3476                tool.authorize(
3477                    &PathBuf::from("link_to_external/config.txt"),
3478                    "edit through symlink",
3479                    &stream_tx,
3480                    cx,
3481                )
3482            })
3483            .await;
3484
3485        assert!(result.is_err(), "Tool should fail when policy denies");
3486        assert!(
3487            !matches!(
3488                stream_rx.try_next(),
3489                Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
3490            ),
3491            "Deny policy should not emit symlink authorization prompt",
3492        );
3493    }
3494
3495    #[gpui::test]
3496    async fn test_streaming_authorize_global_config(cx: &mut TestAppContext) {
3497        init_test(cx);
3498        let fs = project::FakeFs::new(cx.executor());
3499        fs.insert_tree("/project", json!({})).await;
3500        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3501        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3502        let context_server_registry =
3503            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3504        let model = Arc::new(FakeLanguageModel::default());
3505        let thread = cx.new(|cx| {
3506            crate::Thread::new(
3507                project.clone(),
3508                cx.new(|_cx| ProjectContext::default()),
3509                context_server_registry,
3510                Templates::new(),
3511                Some(model.clone()),
3512                cx,
3513            )
3514        });
3515        let tool = Arc::new(StreamingEditFileTool::new(
3516            project.clone(),
3517            thread.downgrade(),
3518            language_registry,
3519        ));
3520
3521        let test_cases = vec![
3522            (
3523                "/etc/hosts",
3524                true,
3525                "System file should require confirmation",
3526            ),
3527            (
3528                "/usr/local/bin/script",
3529                true,
3530                "System bin file should require confirmation",
3531            ),
3532            (
3533                "project/normal_file.rs",
3534                false,
3535                "Normal project file should not require confirmation",
3536            ),
3537        ];
3538
3539        for (path, should_confirm, description) in test_cases {
3540            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3541            let auth =
3542                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3543
3544            if should_confirm {
3545                stream_rx.expect_authorization().await;
3546            } else {
3547                auth.await.unwrap();
3548                assert!(
3549                    stream_rx.try_next().is_err(),
3550                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3551                    description,
3552                    path
3553                );
3554            }
3555        }
3556    }
3557
3558    #[gpui::test]
3559    async fn test_streaming_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
3560        init_test(cx);
3561        let fs = project::FakeFs::new(cx.executor());
3562
3563        fs.insert_tree(
3564            "/workspace/frontend",
3565            json!({
3566                "src": {
3567                    "main.js": "console.log('frontend');"
3568                }
3569            }),
3570        )
3571        .await;
3572        fs.insert_tree(
3573            "/workspace/backend",
3574            json!({
3575                "src": {
3576                    "main.rs": "fn main() {}"
3577                }
3578            }),
3579        )
3580        .await;
3581        fs.insert_tree(
3582            "/workspace/shared",
3583            json!({
3584                ".zed": {
3585                    "settings.json": "{}"
3586                }
3587            }),
3588        )
3589        .await;
3590
3591        let project = Project::test(
3592            fs.clone(),
3593            [
3594                path!("/workspace/frontend").as_ref(),
3595                path!("/workspace/backend").as_ref(),
3596                path!("/workspace/shared").as_ref(),
3597            ],
3598            cx,
3599        )
3600        .await;
3601        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3602        let context_server_registry =
3603            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3604        let model = Arc::new(FakeLanguageModel::default());
3605        let thread = cx.new(|cx| {
3606            crate::Thread::new(
3607                project.clone(),
3608                cx.new(|_cx| ProjectContext::default()),
3609                context_server_registry.clone(),
3610                Templates::new(),
3611                Some(model.clone()),
3612                cx,
3613            )
3614        });
3615        let tool = Arc::new(StreamingEditFileTool::new(
3616            project.clone(),
3617            thread.downgrade(),
3618            language_registry,
3619        ));
3620
3621        let test_cases = vec![
3622            ("frontend/src/main.js", false, "File in first worktree"),
3623            ("backend/src/main.rs", false, "File in second worktree"),
3624            (
3625                "shared/.zed/settings.json",
3626                true,
3627                ".zed file in third worktree",
3628            ),
3629            ("/etc/hosts", true, "Absolute path outside all worktrees"),
3630            (
3631                "../outside/file.txt",
3632                true,
3633                "Relative path outside worktrees",
3634            ),
3635        ];
3636
3637        for (path, should_confirm, description) in test_cases {
3638            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3639            let auth =
3640                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3641
3642            if should_confirm {
3643                stream_rx.expect_authorization().await;
3644            } else {
3645                auth.await.unwrap();
3646                assert!(
3647                    stream_rx.try_next().is_err(),
3648                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3649                    description,
3650                    path
3651                );
3652            }
3653        }
3654    }
3655
3656    #[gpui::test]
3657    async fn test_streaming_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
3658        init_test(cx);
3659        let fs = project::FakeFs::new(cx.executor());
3660        fs.insert_tree(
3661            "/project",
3662            json!({
3663                ".zed": {
3664                    "settings.json": "{}"
3665                },
3666                "src": {
3667                    ".zed": {
3668                        "local.json": "{}"
3669                    }
3670                }
3671            }),
3672        )
3673        .await;
3674        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3675        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3676        let context_server_registry =
3677            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3678        let model = Arc::new(FakeLanguageModel::default());
3679        let thread = cx.new(|cx| {
3680            crate::Thread::new(
3681                project.clone(),
3682                cx.new(|_cx| ProjectContext::default()),
3683                context_server_registry.clone(),
3684                Templates::new(),
3685                Some(model.clone()),
3686                cx,
3687            )
3688        });
3689        let tool = Arc::new(StreamingEditFileTool::new(
3690            project.clone(),
3691            thread.downgrade(),
3692            language_registry,
3693        ));
3694
3695        let test_cases = vec![
3696            ("", false, "Empty path is treated as project root"),
3697            ("/", true, "Root directory should be outside project"),
3698            (
3699                "project/../other",
3700                true,
3701                "Path with .. that goes outside of root directory",
3702            ),
3703            (
3704                "project/./src/file.rs",
3705                false,
3706                "Path with . should work normally",
3707            ),
3708            #[cfg(target_os = "windows")]
3709            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
3710            #[cfg(target_os = "windows")]
3711            ("project\\src\\main.rs", false, "Windows-style project path"),
3712        ];
3713
3714        for (path, should_confirm, description) in test_cases {
3715            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3716            let auth =
3717                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3718
3719            cx.run_until_parked();
3720
3721            if should_confirm {
3722                stream_rx.expect_authorization().await;
3723            } else {
3724                assert!(
3725                    stream_rx.try_next().is_err(),
3726                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3727                    description,
3728                    path
3729                );
3730                auth.await.unwrap();
3731            }
3732        }
3733    }
3734
3735    #[gpui::test]
3736    async fn test_streaming_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
3737        init_test(cx);
3738        let fs = project::FakeFs::new(cx.executor());
3739        fs.insert_tree(
3740            "/project",
3741            json!({
3742                "existing.txt": "content",
3743                ".zed": {
3744                    "settings.json": "{}"
3745                }
3746            }),
3747        )
3748        .await;
3749        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3750        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3751        let context_server_registry =
3752            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3753        let model = Arc::new(FakeLanguageModel::default());
3754        let thread = cx.new(|cx| {
3755            crate::Thread::new(
3756                project.clone(),
3757                cx.new(|_cx| ProjectContext::default()),
3758                context_server_registry.clone(),
3759                Templates::new(),
3760                Some(model.clone()),
3761                cx,
3762            )
3763        });
3764        let tool = Arc::new(StreamingEditFileTool::new(
3765            project.clone(),
3766            thread.downgrade(),
3767            language_registry,
3768        ));
3769
3770        let modes = vec![StreamingEditFileMode::Edit, StreamingEditFileMode::Write];
3771
3772        for _mode in modes {
3773            // Test .zed path with different modes
3774            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3775            let _auth = cx.update(|cx| {
3776                tool.authorize(
3777                    &PathBuf::from("project/.zed/settings.json"),
3778                    "Edit settings",
3779                    &stream_tx,
3780                    cx,
3781                )
3782            });
3783
3784            stream_rx.expect_authorization().await;
3785
3786            // Test outside path with different modes
3787            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3788            let _auth = cx.update(|cx| {
3789                tool.authorize(
3790                    &PathBuf::from("/outside/file.txt"),
3791                    "Edit file",
3792                    &stream_tx,
3793                    cx,
3794                )
3795            });
3796
3797            stream_rx.expect_authorization().await;
3798
3799            // Test normal path with different modes
3800            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3801            cx.update(|cx| {
3802                tool.authorize(
3803                    &PathBuf::from("project/normal.txt"),
3804                    "Edit file",
3805                    &stream_tx,
3806                    cx,
3807                )
3808            })
3809            .await
3810            .unwrap();
3811            assert!(stream_rx.try_next().is_err());
3812        }
3813    }
3814
3815    #[gpui::test]
3816    async fn test_streaming_initial_title_with_partial_input(cx: &mut TestAppContext) {
3817        init_test(cx);
3818        let fs = project::FakeFs::new(cx.executor());
3819        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3820        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3821        let context_server_registry =
3822            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3823        let model = Arc::new(FakeLanguageModel::default());
3824        let thread = cx.new(|cx| {
3825            crate::Thread::new(
3826                project.clone(),
3827                cx.new(|_cx| ProjectContext::default()),
3828                context_server_registry,
3829                Templates::new(),
3830                Some(model.clone()),
3831                cx,
3832            )
3833        });
3834        let tool = Arc::new(StreamingEditFileTool::new(
3835            project,
3836            thread.downgrade(),
3837            language_registry,
3838        ));
3839
3840        cx.update(|cx| {
3841            assert_eq!(
3842                tool.initial_title(
3843                    Err(json!({
3844                        "path": "src/main.rs",
3845                        "display_description": "",
3846                    })),
3847                    cx
3848                ),
3849                "src/main.rs"
3850            );
3851            assert_eq!(
3852                tool.initial_title(
3853                    Err(json!({
3854                        "path": "",
3855                        "display_description": "Fix error handling",
3856                    })),
3857                    cx
3858                ),
3859                "Fix error handling"
3860            );
3861            assert_eq!(
3862                tool.initial_title(
3863                    Err(json!({
3864                        "path": "src/main.rs",
3865                        "display_description": "Fix error handling",
3866                    })),
3867                    cx
3868                ),
3869                "src/main.rs"
3870            );
3871            assert_eq!(
3872                tool.initial_title(
3873                    Err(json!({
3874                        "path": "",
3875                        "display_description": "",
3876                    })),
3877                    cx
3878                ),
3879                DEFAULT_UI_TEXT
3880            );
3881            assert_eq!(
3882                tool.initial_title(Err(serde_json::Value::Null), cx),
3883                DEFAULT_UI_TEXT
3884            );
3885        });
3886    }
3887
3888    #[gpui::test]
3889    async fn test_streaming_diff_finalization(cx: &mut TestAppContext) {
3890        init_test(cx);
3891        let fs = project::FakeFs::new(cx.executor());
3892        fs.insert_tree("/", json!({"main.rs": ""})).await;
3893
3894        let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
3895        let languages = project.read_with(cx, |project, _cx| project.languages().clone());
3896        let context_server_registry =
3897            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3898        let model = Arc::new(FakeLanguageModel::default());
3899        let thread = cx.new(|cx| {
3900            crate::Thread::new(
3901                project.clone(),
3902                cx.new(|_cx| ProjectContext::default()),
3903                context_server_registry.clone(),
3904                Templates::new(),
3905                Some(model.clone()),
3906                cx,
3907            )
3908        });
3909
3910        // Ensure the diff is finalized after the edit completes.
3911        {
3912            let tool = Arc::new(StreamingEditFileTool::new(
3913                project.clone(),
3914                thread.downgrade(),
3915                languages.clone(),
3916            ));
3917            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3918            let edit = cx.update(|cx| {
3919                tool.run(
3920                    ToolInput::resolved(StreamingEditFileToolInput {
3921                        display_description: "Edit file".into(),
3922                        path: path!("/main.rs").into(),
3923                        mode: StreamingEditFileMode::Write,
3924                        content: Some("new content".into()),
3925                        edits: None,
3926                    }),
3927                    stream_tx,
3928                    cx,
3929                )
3930            });
3931            stream_rx.expect_update_fields().await;
3932            let diff = stream_rx.expect_diff().await;
3933            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
3934            cx.run_until_parked();
3935            edit.await.unwrap();
3936            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
3937        }
3938
3939        // Ensure the diff is finalized if the tool call gets dropped.
3940        {
3941            let tool = Arc::new(StreamingEditFileTool::new(
3942                project.clone(),
3943                thread.downgrade(),
3944                languages.clone(),
3945            ));
3946            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3947            let edit = cx.update(|cx| {
3948                tool.run(
3949                    ToolInput::resolved(StreamingEditFileToolInput {
3950                        display_description: "Edit file".into(),
3951                        path: path!("/main.rs").into(),
3952                        mode: StreamingEditFileMode::Write,
3953                        content: Some("dropped content".into()),
3954                        edits: None,
3955                    }),
3956                    stream_tx,
3957                    cx,
3958                )
3959            });
3960            stream_rx.expect_update_fields().await;
3961            let diff = stream_rx.expect_diff().await;
3962            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
3963            drop(edit);
3964            cx.run_until_parked();
3965            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
3966        }
3967    }
3968
3969    #[gpui::test]
3970    async fn test_streaming_consecutive_edits_work(cx: &mut TestAppContext) {
3971        init_test(cx);
3972
3973        let fs = project::FakeFs::new(cx.executor());
3974        fs.insert_tree(
3975            "/root",
3976            json!({
3977                "test.txt": "original content"
3978            }),
3979        )
3980        .await;
3981        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3982        let context_server_registry =
3983            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3984        let model = Arc::new(FakeLanguageModel::default());
3985        let thread = cx.new(|cx| {
3986            crate::Thread::new(
3987                project.clone(),
3988                cx.new(|_cx| ProjectContext::default()),
3989                context_server_registry,
3990                Templates::new(),
3991                Some(model.clone()),
3992                cx,
3993            )
3994        });
3995        let languages = project.read_with(cx, |project, _| project.languages().clone());
3996        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
3997
3998        let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true));
3999        let edit_tool = Arc::new(StreamingEditFileTool::new(
4000            project.clone(),
4001            thread.downgrade(),
4002            languages,
4003        ));
4004
4005        // Read the file first
4006        cx.update(|cx| {
4007            read_tool.clone().run(
4008                ToolInput::resolved(crate::ReadFileToolInput {
4009                    path: "root/test.txt".to_string(),
4010                    start_line: None,
4011                    end_line: None,
4012                }),
4013                ToolCallEventStream::test().0,
4014                cx,
4015            )
4016        })
4017        .await
4018        .unwrap();
4019
4020        // First edit should work
4021        let edit_result = cx
4022            .update(|cx| {
4023                edit_tool.clone().run(
4024                    ToolInput::resolved(StreamingEditFileToolInput {
4025                        display_description: "First edit".into(),
4026                        path: "root/test.txt".into(),
4027                        mode: StreamingEditFileMode::Edit,
4028                        content: None,
4029                        edits: Some(vec![Edit {
4030                            old_text: "original content".into(),
4031                            new_text: "modified content".into(),
4032                        }]),
4033                    }),
4034                    ToolCallEventStream::test().0,
4035                    cx,
4036                )
4037            })
4038            .await;
4039        assert!(
4040            edit_result.is_ok(),
4041            "First edit should succeed, got error: {:?}",
4042            edit_result.as_ref().err()
4043        );
4044
4045        // Second edit should also work because the edit updated the recorded read time
4046        let edit_result = cx
4047            .update(|cx| {
4048                edit_tool.clone().run(
4049                    ToolInput::resolved(StreamingEditFileToolInput {
4050                        display_description: "Second edit".into(),
4051                        path: "root/test.txt".into(),
4052                        mode: StreamingEditFileMode::Edit,
4053                        content: None,
4054                        edits: Some(vec![Edit {
4055                            old_text: "modified content".into(),
4056                            new_text: "further modified content".into(),
4057                        }]),
4058                    }),
4059                    ToolCallEventStream::test().0,
4060                    cx,
4061                )
4062            })
4063            .await;
4064        assert!(
4065            edit_result.is_ok(),
4066            "Second consecutive edit should succeed, got error: {:?}",
4067            edit_result.as_ref().err()
4068        );
4069    }
4070
4071    #[gpui::test]
4072    async fn test_streaming_external_modification_detected(cx: &mut TestAppContext) {
4073        init_test(cx);
4074
4075        let fs = project::FakeFs::new(cx.executor());
4076        fs.insert_tree(
4077            "/root",
4078            json!({
4079                "test.txt": "original content"
4080            }),
4081        )
4082        .await;
4083        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4084        let context_server_registry =
4085            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4086        let model = Arc::new(FakeLanguageModel::default());
4087        let thread = cx.new(|cx| {
4088            crate::Thread::new(
4089                project.clone(),
4090                cx.new(|_cx| ProjectContext::default()),
4091                context_server_registry,
4092                Templates::new(),
4093                Some(model.clone()),
4094                cx,
4095            )
4096        });
4097        let languages = project.read_with(cx, |project, _| project.languages().clone());
4098        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4099
4100        let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true));
4101        let edit_tool = Arc::new(StreamingEditFileTool::new(
4102            project.clone(),
4103            thread.downgrade(),
4104            languages,
4105        ));
4106
4107        // Read the file first
4108        cx.update(|cx| {
4109            read_tool.clone().run(
4110                ToolInput::resolved(crate::ReadFileToolInput {
4111                    path: "root/test.txt".to_string(),
4112                    start_line: None,
4113                    end_line: None,
4114                }),
4115                ToolCallEventStream::test().0,
4116                cx,
4117            )
4118        })
4119        .await
4120        .unwrap();
4121
4122        // Simulate external modification
4123        cx.background_executor
4124            .advance_clock(std::time::Duration::from_secs(2));
4125        fs.save(
4126            path!("/root/test.txt").as_ref(),
4127            &"externally modified content".into(),
4128            language::LineEnding::Unix,
4129        )
4130        .await
4131        .unwrap();
4132
4133        // Reload the buffer to pick up the new mtime
4134        let project_path = project
4135            .read_with(cx, |project, cx| {
4136                project.find_project_path("root/test.txt", cx)
4137            })
4138            .expect("Should find project path");
4139        let buffer = project
4140            .update(cx, |project, cx| project.open_buffer(project_path, cx))
4141            .await
4142            .unwrap();
4143        buffer
4144            .update(cx, |buffer, cx| buffer.reload(cx))
4145            .await
4146            .unwrap();
4147
4148        cx.executor().run_until_parked();
4149
4150        // Try to edit - should fail because file was modified externally
4151        let result = cx
4152            .update(|cx| {
4153                edit_tool.clone().run(
4154                    ToolInput::resolved(StreamingEditFileToolInput {
4155                        display_description: "Edit after external change".into(),
4156                        path: "root/test.txt".into(),
4157                        mode: StreamingEditFileMode::Edit,
4158                        content: None,
4159                        edits: Some(vec![Edit {
4160                            old_text: "externally modified content".into(),
4161                            new_text: "new content".into(),
4162                        }]),
4163                    }),
4164                    ToolCallEventStream::test().0,
4165                    cx,
4166                )
4167            })
4168            .await;
4169
4170        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
4171            panic!("expected error");
4172        };
4173        assert!(
4174            error.contains("has been modified since you last read it"),
4175            "Error should mention file modification, got: {}",
4176            error
4177        );
4178    }
4179
4180    #[gpui::test]
4181    async fn test_streaming_dirty_buffer_detected(cx: &mut TestAppContext) {
4182        init_test(cx);
4183
4184        let fs = project::FakeFs::new(cx.executor());
4185        fs.insert_tree(
4186            "/root",
4187            json!({
4188                "test.txt": "original content"
4189            }),
4190        )
4191        .await;
4192        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4193        let context_server_registry =
4194            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4195        let model = Arc::new(FakeLanguageModel::default());
4196        let thread = cx.new(|cx| {
4197            crate::Thread::new(
4198                project.clone(),
4199                cx.new(|_cx| ProjectContext::default()),
4200                context_server_registry,
4201                Templates::new(),
4202                Some(model.clone()),
4203                cx,
4204            )
4205        });
4206        let languages = project.read_with(cx, |project, _| project.languages().clone());
4207        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4208
4209        let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true));
4210        let edit_tool = Arc::new(StreamingEditFileTool::new(
4211            project.clone(),
4212            thread.downgrade(),
4213            languages,
4214        ));
4215
4216        // Read the file first
4217        cx.update(|cx| {
4218            read_tool.clone().run(
4219                ToolInput::resolved(crate::ReadFileToolInput {
4220                    path: "root/test.txt".to_string(),
4221                    start_line: None,
4222                    end_line: None,
4223                }),
4224                ToolCallEventStream::test().0,
4225                cx,
4226            )
4227        })
4228        .await
4229        .unwrap();
4230
4231        // Open the buffer and make it dirty
4232        let project_path = project
4233            .read_with(cx, |project, cx| {
4234                project.find_project_path("root/test.txt", cx)
4235            })
4236            .expect("Should find project path");
4237        let buffer = project
4238            .update(cx, |project, cx| project.open_buffer(project_path, cx))
4239            .await
4240            .unwrap();
4241
4242        buffer.update(cx, |buffer, cx| {
4243            let end_point = buffer.max_point();
4244            buffer.edit([(end_point..end_point, " added text")], None, cx);
4245        });
4246
4247        let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
4248        assert!(is_dirty, "Buffer should be dirty after in-memory edit");
4249
4250        // Try to edit - should fail because buffer has unsaved changes
4251        let result = cx
4252            .update(|cx| {
4253                edit_tool.clone().run(
4254                    ToolInput::resolved(StreamingEditFileToolInput {
4255                        display_description: "Edit with dirty buffer".into(),
4256                        path: "root/test.txt".into(),
4257                        mode: StreamingEditFileMode::Edit,
4258                        content: None,
4259                        edits: Some(vec![Edit {
4260                            old_text: "original content".into(),
4261                            new_text: "new content".into(),
4262                        }]),
4263                    }),
4264                    ToolCallEventStream::test().0,
4265                    cx,
4266                )
4267            })
4268            .await;
4269
4270        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
4271            panic!("expected error");
4272        };
4273        assert!(
4274            error.contains("This file has unsaved changes."),
4275            "Error should mention unsaved changes, got: {}",
4276            error
4277        );
4278        assert!(
4279            error.contains("keep or discard"),
4280            "Error should ask whether to keep or discard changes, got: {}",
4281            error
4282        );
4283        assert!(
4284            error.contains("save or revert the file manually"),
4285            "Error should ask user to manually save or revert when tools aren't available, got: {}",
4286            error
4287        );
4288    }
4289
4290    #[gpui::test]
4291    async fn test_streaming_overlapping_edits_resolved_sequentially(cx: &mut TestAppContext) {
4292        init_test(cx);
4293
4294        let fs = project::FakeFs::new(cx.executor());
4295        // Edit 1's replacement introduces text that contains edit 2's
4296        // old_text as a substring. Because edits resolve sequentially
4297        // against the current buffer, edit 2 finds a unique match in
4298        // the modified buffer and succeeds.
4299        fs.insert_tree(
4300            "/root",
4301            json!({
4302                "file.txt": "aaa\nbbb\nccc\nddd\neee\n"
4303            }),
4304        )
4305        .await;
4306        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4307        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4308        let context_server_registry =
4309            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4310        let model = Arc::new(FakeLanguageModel::default());
4311        let thread = cx.new(|cx| {
4312            crate::Thread::new(
4313                project.clone(),
4314                cx.new(|_cx| ProjectContext::default()),
4315                context_server_registry,
4316                Templates::new(),
4317                Some(model),
4318                cx,
4319            )
4320        });
4321
4322        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4323        let (event_stream, _receiver) = ToolCallEventStream::test();
4324
4325        let tool = Arc::new(StreamingEditFileTool::new(
4326            project.clone(),
4327            thread.downgrade(),
4328            language_registry,
4329        ));
4330
4331        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4332
4333        // Setup: resolve the buffer
4334        sender.send_partial(json!({
4335            "display_description": "Overlapping edits",
4336            "path": "root/file.txt",
4337            "mode": "edit"
4338        }));
4339        cx.run_until_parked();
4340
4341        // Edit 1 replaces "bbb\nccc" with "XXX\nccc\nddd", so the
4342        // buffer becomes "aaa\nXXX\nccc\nddd\nddd\neee\n".
4343        // Edit 2's old_text "ccc\nddd" matches the first occurrence
4344        // in the modified buffer and replaces it with "ZZZ".
4345        // Edit 3 exists only to mark edit 2 as "complete" during streaming.
4346        sender.send_partial(json!({
4347            "display_description": "Overlapping edits",
4348            "path": "root/file.txt",
4349            "mode": "edit",
4350            "edits": [
4351                {"old_text": "bbb\nccc", "new_text": "XXX\nccc\nddd"},
4352                {"old_text": "ccc\nddd", "new_text": "ZZZ"},
4353                {"old_text": "eee", "new_text": "DUMMY"}
4354            ]
4355        }));
4356        cx.run_until_parked();
4357
4358        // Send the final input with all three edits.
4359        sender.send_final(json!({
4360            "display_description": "Overlapping edits",
4361            "path": "root/file.txt",
4362            "mode": "edit",
4363            "edits": [
4364                {"old_text": "bbb\nccc", "new_text": "XXX\nccc\nddd"},
4365                {"old_text": "ccc\nddd", "new_text": "ZZZ"},
4366                {"old_text": "eee", "new_text": "DUMMY"}
4367            ]
4368        }));
4369
4370        let result = task.await;
4371        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4372            panic!("expected success");
4373        };
4374        assert_eq!(new_text, "aaa\nXXX\nZZZ\nddd\nDUMMY\n");
4375    }
4376
4377    #[gpui::test]
4378    async fn test_streaming_create_content_streamed(cx: &mut TestAppContext) {
4379        init_test(cx);
4380
4381        let fs = project::FakeFs::new(cx.executor());
4382        fs.insert_tree("/root", json!({"dir": {}})).await;
4383        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4384        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4385        let context_server_registry =
4386            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4387        let model = Arc::new(FakeLanguageModel::default());
4388        let thread = cx.new(|cx| {
4389            crate::Thread::new(
4390                project.clone(),
4391                cx.new(|_cx| ProjectContext::default()),
4392                context_server_registry,
4393                Templates::new(),
4394                Some(model),
4395                cx,
4396            )
4397        });
4398
4399        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4400        let (event_stream, _receiver) = ToolCallEventStream::test();
4401
4402        let tool = Arc::new(StreamingEditFileTool::new(
4403            project.clone(),
4404            thread.downgrade(),
4405            language_registry,
4406        ));
4407
4408        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4409
4410        // Transition to BufferResolved
4411        sender.send_partial(json!({
4412            "display_description": "Create new file",
4413            "path": "root/dir/new_file.txt",
4414            "mode": "write"
4415        }));
4416        cx.run_until_parked();
4417
4418        // Stream content incrementally
4419        sender.send_partial(json!({
4420            "display_description": "Create new file",
4421            "path": "root/dir/new_file.txt",
4422            "mode": "write",
4423            "content": "line 1\n"
4424        }));
4425        cx.run_until_parked();
4426
4427        // Verify buffer has partial content
4428        let buffer = project.update(cx, |project, cx| {
4429            let path = project
4430                .find_project_path("root/dir/new_file.txt", cx)
4431                .unwrap();
4432            project.get_open_buffer(&path, cx).unwrap()
4433        });
4434        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\n");
4435
4436        // Stream more content
4437        sender.send_partial(json!({
4438            "display_description": "Create new file",
4439            "path": "root/dir/new_file.txt",
4440            "mode": "write",
4441            "content": "line 1\nline 2\n"
4442        }));
4443        cx.run_until_parked();
4444        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\nline 2\n");
4445
4446        // Stream final chunk
4447        sender.send_partial(json!({
4448            "display_description": "Create new file",
4449            "path": "root/dir/new_file.txt",
4450            "mode": "write",
4451            "content": "line 1\nline 2\nline 3\n"
4452        }));
4453        cx.run_until_parked();
4454        assert_eq!(
4455            buffer.read_with(cx, |b, _| b.text()),
4456            "line 1\nline 2\nline 3\n"
4457        );
4458
4459        // Send final input
4460        sender.send_final(json!({
4461            "display_description": "Create new file",
4462            "path": "root/dir/new_file.txt",
4463            "mode": "write",
4464            "content": "line 1\nline 2\nline 3\n"
4465        }));
4466
4467        let result = task.await;
4468        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4469            panic!("expected success");
4470        };
4471        assert_eq!(new_text, "line 1\nline 2\nline 3\n");
4472    }
4473
4474    #[gpui::test]
4475    async fn test_streaming_overwrite_diff_revealed_during_streaming(cx: &mut TestAppContext) {
4476        init_test(cx);
4477
4478        let fs = project::FakeFs::new(cx.executor());
4479        fs.insert_tree(
4480            "/root",
4481            json!({
4482                "file.txt": "old line 1\nold line 2\nold line 3\n"
4483            }),
4484        )
4485        .await;
4486        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4487        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4488        let context_server_registry =
4489            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4490        let model = Arc::new(FakeLanguageModel::default());
4491        let thread = cx.new(|cx| {
4492            crate::Thread::new(
4493                project.clone(),
4494                cx.new(|_cx| ProjectContext::default()),
4495                context_server_registry,
4496                Templates::new(),
4497                Some(model),
4498                cx,
4499            )
4500        });
4501
4502        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4503        let (event_stream, mut receiver) = ToolCallEventStream::test();
4504
4505        let tool = Arc::new(StreamingEditFileTool::new(
4506            project.clone(),
4507            thread.downgrade(),
4508            language_registry,
4509        ));
4510
4511        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4512
4513        // Transition to BufferResolved
4514        sender.send_partial(json!({
4515            "display_description": "Overwrite file",
4516            "path": "root/file.txt",
4517            "mode": "write"
4518        }));
4519        cx.run_until_parked();
4520
4521        // Get the diff entity from the event stream
4522        receiver.expect_update_fields().await;
4523        let diff = receiver.expect_diff().await;
4524
4525        // Diff starts pending with no revealed ranges
4526        diff.read_with(cx, |diff, cx| {
4527            assert!(matches!(diff, Diff::Pending(_)));
4528            assert!(!diff.has_revealed_range(cx));
4529        });
4530
4531        // Stream first content chunk
4532        sender.send_partial(json!({
4533            "display_description": "Overwrite file",
4534            "path": "root/file.txt",
4535            "mode": "write",
4536            "content": "new line 1\n"
4537        }));
4538        cx.run_until_parked();
4539
4540        // Diff should now have revealed ranges showing the new content
4541        diff.read_with(cx, |diff, cx| {
4542            assert!(diff.has_revealed_range(cx));
4543        });
4544
4545        // Send final input
4546        sender.send_final(json!({
4547            "display_description": "Overwrite file",
4548            "path": "root/file.txt",
4549            "mode": "write",
4550            "content": "new line 1\nnew line 2\n"
4551        }));
4552
4553        let result = task.await;
4554        let StreamingEditFileToolOutput::Success {
4555            new_text, old_text, ..
4556        } = result.unwrap()
4557        else {
4558            panic!("expected success");
4559        };
4560        assert_eq!(new_text, "new line 1\nnew line 2\n");
4561        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
4562
4563        // Diff is finalized after completion
4564        diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
4565    }
4566
4567    #[gpui::test]
4568    async fn test_streaming_overwrite_content_streamed(cx: &mut TestAppContext) {
4569        init_test(cx);
4570
4571        let fs = project::FakeFs::new(cx.executor());
4572        fs.insert_tree(
4573            "/root",
4574            json!({
4575                "file.txt": "old line 1\nold line 2\nold line 3\n"
4576            }),
4577        )
4578        .await;
4579        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4580        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4581        let context_server_registry =
4582            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4583        let model = Arc::new(FakeLanguageModel::default());
4584        let thread = cx.new(|cx| {
4585            crate::Thread::new(
4586                project.clone(),
4587                cx.new(|_cx| ProjectContext::default()),
4588                context_server_registry,
4589                Templates::new(),
4590                Some(model),
4591                cx,
4592            )
4593        });
4594
4595        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4596        let (event_stream, _receiver) = ToolCallEventStream::test();
4597
4598        let tool = Arc::new(StreamingEditFileTool::new(
4599            project.clone(),
4600            thread.downgrade(),
4601            language_registry,
4602        ));
4603
4604        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4605
4606        // Transition to BufferResolved
4607        sender.send_partial(json!({
4608            "display_description": "Overwrite file",
4609            "path": "root/file.txt",
4610            "mode": "write"
4611        }));
4612        cx.run_until_parked();
4613
4614        // Verify buffer still has old content (no content partial yet)
4615        let buffer = project.update(cx, |project, cx| {
4616            let path = project.find_project_path("root/file.txt", cx).unwrap();
4617            project.get_open_buffer(&path, cx).unwrap()
4618        });
4619        assert_eq!(
4620            buffer.read_with(cx, |b, _| b.text()),
4621            "old line 1\nold line 2\nold line 3\n"
4622        );
4623
4624        // First content partial replaces old content
4625        sender.send_partial(json!({
4626            "display_description": "Overwrite file",
4627            "path": "root/file.txt",
4628            "mode": "write",
4629            "content": "new line 1\n"
4630        }));
4631        cx.run_until_parked();
4632        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "new line 1\n");
4633
4634        // Subsequent content partials append
4635        sender.send_partial(json!({
4636            "display_description": "Overwrite file",
4637            "path": "root/file.txt",
4638            "mode": "write",
4639            "content": "new line 1\nnew line 2\n"
4640        }));
4641        cx.run_until_parked();
4642        assert_eq!(
4643            buffer.read_with(cx, |b, _| b.text()),
4644            "new line 1\nnew line 2\n"
4645        );
4646
4647        // Send final input with complete content
4648        sender.send_final(json!({
4649            "display_description": "Overwrite file",
4650            "path": "root/file.txt",
4651            "mode": "write",
4652            "content": "new line 1\nnew line 2\nnew line 3\n"
4653        }));
4654
4655        let result = task.await;
4656        let StreamingEditFileToolOutput::Success {
4657            new_text, old_text, ..
4658        } = result.unwrap()
4659        else {
4660            panic!("expected success");
4661        };
4662        assert_eq!(new_text, "new line 1\nnew line 2\nnew line 3\n");
4663        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
4664    }
4665
4666    #[gpui::test]
4667    async fn test_streaming_edit_json_fixer_escape_corruption(cx: &mut TestAppContext) {
4668        init_test(cx);
4669
4670        let fs = project::FakeFs::new(cx.executor());
4671        fs.insert_tree(
4672            "/root",
4673            json!({
4674                "file.txt": "hello\nworld\nfoo\n"
4675            }),
4676        )
4677        .await;
4678        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4679        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4680        let context_server_registry =
4681            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4682        let model = Arc::new(FakeLanguageModel::default());
4683        let thread = cx.new(|cx| {
4684            crate::Thread::new(
4685                project.clone(),
4686                cx.new(|_cx| ProjectContext::default()),
4687                context_server_registry,
4688                Templates::new(),
4689                Some(model),
4690                cx,
4691            )
4692        });
4693
4694        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4695        let (event_stream, _receiver) = ToolCallEventStream::test();
4696
4697        let tool = Arc::new(StreamingEditFileTool::new(
4698            project.clone(),
4699            thread.downgrade(),
4700            language_registry,
4701        ));
4702
4703        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4704
4705        sender.send_partial(json!({
4706            "display_description": "Edit",
4707            "path": "root/file.txt",
4708            "mode": "edit"
4709        }));
4710        cx.run_until_parked();
4711
4712        // Simulate JSON fixer producing a literal backslash when the LLM
4713        // stream cuts in the middle of a \n escape sequence.
4714        // The old_text "hello\nworld" would be streamed as:
4715        //   partial 1: old_text = "hello\\" (fixer closes incomplete \n as \\)
4716        //   partial 2: old_text = "hello\nworld" (fixer corrected the escape)
4717        sender.send_partial(json!({
4718            "display_description": "Edit",
4719            "path": "root/file.txt",
4720            "mode": "edit",
4721            "edits": [{"old_text": "hello\\"}]
4722        }));
4723        cx.run_until_parked();
4724
4725        // Now the fixer corrects it to the real newline.
4726        sender.send_partial(json!({
4727            "display_description": "Edit",
4728            "path": "root/file.txt",
4729            "mode": "edit",
4730            "edits": [{"old_text": "hello\nworld"}]
4731        }));
4732        cx.run_until_parked();
4733
4734        // Send final.
4735        sender.send_final(json!({
4736            "display_description": "Edit",
4737            "path": "root/file.txt",
4738            "mode": "edit",
4739            "edits": [{"old_text": "hello\nworld", "new_text": "HELLO\nWORLD"}]
4740        }));
4741
4742        let result = task.await;
4743        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4744            panic!("expected success");
4745        };
4746        assert_eq!(new_text, "HELLO\nWORLD\nfoo\n");
4747    }
4748
4749    // Verifies that after streaming_edit_file_tool edits a file, the action log
4750    // reports changed buffers so that the Accept All / Reject All review UI appears.
4751    #[gpui::test]
4752    async fn test_streaming_edit_file_tool_registers_changed_buffers(cx: &mut TestAppContext) {
4753        init_test(cx);
4754        cx.update(|cx| {
4755            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
4756            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
4757            agent_settings::AgentSettings::override_global(settings, cx);
4758        });
4759
4760        let fs = project::FakeFs::new(cx.executor());
4761        fs.insert_tree(
4762            path!("/root"),
4763            json!({
4764                "file.txt": "line 1\nline 2\nline 3\n"
4765            }),
4766        )
4767        .await;
4768
4769        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4770        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4771        let context_server_registry =
4772            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4773        let thread = cx.new(|cx| {
4774            crate::Thread::new(
4775                project.clone(),
4776                cx.new(|_cx| ProjectContext::default()),
4777                context_server_registry,
4778                Templates::new(),
4779                None,
4780                cx,
4781            )
4782        });
4783        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4784
4785        let tool = Arc::new(StreamingEditFileTool::new(
4786            project.clone(),
4787            thread.downgrade(),
4788            language_registry,
4789        ));
4790        let (event_stream, _rx) = ToolCallEventStream::test();
4791
4792        let task = cx.update(|cx| {
4793            tool.run(
4794                ToolInput::resolved(StreamingEditFileToolInput {
4795                    display_description: "Edit lines".to_string(),
4796                    path: "root/file.txt".into(),
4797                    mode: StreamingEditFileMode::Edit,
4798                    content: None,
4799                    edits: Some(vec![Edit {
4800                        old_text: "line 2".into(),
4801                        new_text: "modified line 2".into(),
4802                    }]),
4803                }),
4804                event_stream,
4805                cx,
4806            )
4807        });
4808
4809        let result = task.await;
4810        assert!(result.is_ok(), "edit should succeed: {:?}", result.err());
4811
4812        cx.run_until_parked();
4813
4814        let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
4815        assert!(
4816            !changed.is_empty(),
4817            "action_log.changed_buffers() should be non-empty after streaming edit, \
4818             but no changed buffers were found \u{2014} Accept All / Reject All will not appear"
4819        );
4820    }
4821
4822    // Same test but for Write mode (overwrite entire file).
4823    #[gpui::test]
4824    async fn test_streaming_edit_file_tool_write_mode_registers_changed_buffers(
4825        cx: &mut TestAppContext,
4826    ) {
4827        init_test(cx);
4828        cx.update(|cx| {
4829            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
4830            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
4831            agent_settings::AgentSettings::override_global(settings, cx);
4832        });
4833
4834        let fs = project::FakeFs::new(cx.executor());
4835        fs.insert_tree(
4836            path!("/root"),
4837            json!({
4838                "file.txt": "original content"
4839            }),
4840        )
4841        .await;
4842
4843        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4844        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4845        let context_server_registry =
4846            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4847        let thread = cx.new(|cx| {
4848            crate::Thread::new(
4849                project.clone(),
4850                cx.new(|_cx| ProjectContext::default()),
4851                context_server_registry,
4852                Templates::new(),
4853                None,
4854                cx,
4855            )
4856        });
4857        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4858
4859        let tool = Arc::new(StreamingEditFileTool::new(
4860            project.clone(),
4861            thread.downgrade(),
4862            language_registry,
4863        ));
4864        let (event_stream, _rx) = ToolCallEventStream::test();
4865
4866        let task = cx.update(|cx| {
4867            tool.run(
4868                ToolInput::resolved(StreamingEditFileToolInput {
4869                    display_description: "Overwrite file".to_string(),
4870                    path: "root/file.txt".into(),
4871                    mode: StreamingEditFileMode::Write,
4872                    content: Some("completely new content".into()),
4873                    edits: None,
4874                }),
4875                event_stream,
4876                cx,
4877            )
4878        });
4879
4880        let result = task.await;
4881        assert!(result.is_ok(), "write should succeed: {:?}", result.err());
4882
4883        cx.run_until_parked();
4884
4885        let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
4886        assert!(
4887            !changed.is_empty(),
4888            "action_log.changed_buffers() should be non-empty after streaming write, \
4889             but no changed buffers were found \u{2014} Accept All / Reject All will not appear"
4890        );
4891    }
4892
4893    fn init_test(cx: &mut TestAppContext) {
4894        cx.update(|cx| {
4895            let settings_store = SettingsStore::test(cx);
4896            cx.set_global(settings_store);
4897            SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
4898                store.update_user_settings(cx, |settings| {
4899                    settings
4900                        .project
4901                        .all_languages
4902                        .defaults
4903                        .ensure_final_newline_on_save = Some(false);
4904                });
4905            });
4906        });
4907    }
4908}