edit_agent.rs

   1mod edit_parser;
   2#[cfg(test)]
   3mod evals;
   4
   5use crate::{Template, Templates};
   6use aho_corasick::AhoCorasick;
   7use anyhow::Result;
   8use assistant_tool::ActionLog;
   9use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
  10use futures::{
  11    Stream, StreamExt,
  12    channel::mpsc::{self, UnboundedReceiver},
  13    pin_mut,
  14    stream::BoxStream,
  15};
  16use gpui::{AppContext, AsyncApp, Entity, SharedString, Task};
  17use language::{Bias, Buffer, BufferSnapshot, LineIndent, Point};
  18use language_model::{
  19    LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
  20    MessageContent, Role,
  21};
  22use project::{AgentLocation, Project};
  23use serde::Serialize;
  24use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
  25use streaming_diff::{CharOperation, StreamingDiff};
  26
  27#[derive(Serialize)]
  28struct CreateFilePromptTemplate {
  29    path: Option<PathBuf>,
  30    edit_description: String,
  31}
  32
  33impl Template for CreateFilePromptTemplate {
  34    const TEMPLATE_NAME: &'static str = "create_file_prompt.hbs";
  35}
  36
  37#[derive(Serialize)]
  38struct EditFilePromptTemplate {
  39    path: Option<PathBuf>,
  40    edit_description: String,
  41}
  42
  43impl Template for EditFilePromptTemplate {
  44    const TEMPLATE_NAME: &'static str = "edit_file_prompt.hbs";
  45}
  46
  47#[derive(Clone, Debug, PartialEq, Eq)]
  48pub enum EditAgentOutputEvent {
  49    Edited,
  50    OldTextNotFound(SharedString),
  51}
  52
  53#[derive(Clone, Debug)]
  54pub struct EditAgentOutput {
  55    pub _raw_edits: String,
  56    pub _parser_metrics: EditParserMetrics,
  57}
  58
  59#[derive(Clone)]
  60pub struct EditAgent {
  61    model: Arc<dyn LanguageModel>,
  62    action_log: Entity<ActionLog>,
  63    project: Entity<Project>,
  64    templates: Arc<Templates>,
  65}
  66
  67impl EditAgent {
  68    pub fn new(
  69        model: Arc<dyn LanguageModel>,
  70        project: Entity<Project>,
  71        action_log: Entity<ActionLog>,
  72        templates: Arc<Templates>,
  73    ) -> Self {
  74        EditAgent {
  75            model,
  76            project,
  77            action_log,
  78            templates,
  79        }
  80    }
  81
  82    pub fn overwrite(
  83        &self,
  84        buffer: Entity<Buffer>,
  85        edit_description: String,
  86        previous_messages: Vec<LanguageModelRequestMessage>,
  87        cx: &mut AsyncApp,
  88    ) -> (
  89        Task<Result<EditAgentOutput>>,
  90        mpsc::UnboundedReceiver<EditAgentOutputEvent>,
  91    ) {
  92        let this = self.clone();
  93        let (events_tx, events_rx) = mpsc::unbounded();
  94        let output = cx.spawn(async move |cx| {
  95            let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
  96            let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
  97            let prompt = CreateFilePromptTemplate {
  98                path,
  99                edit_description,
 100            }
 101            .render(&this.templates)?;
 102            let new_chunks = this.request(previous_messages, prompt, cx).await?;
 103
 104            let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
 105            while let Some(event) = inner_events.next().await {
 106                events_tx.unbounded_send(event).ok();
 107            }
 108            output.await
 109        });
 110        (output, events_rx)
 111    }
 112
 113    fn overwrite_with_chunks(
 114        &self,
 115        buffer: Entity<Buffer>,
 116        edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
 117        cx: &mut AsyncApp,
 118    ) -> (
 119        Task<Result<EditAgentOutput>>,
 120        mpsc::UnboundedReceiver<EditAgentOutputEvent>,
 121    ) {
 122        let (output_events_tx, output_events_rx) = mpsc::unbounded();
 123        let this = self.clone();
 124        let task = cx.spawn(async move |cx| {
 125            this.action_log
 126                .update(cx, |log, cx| log.buffer_created(buffer.clone(), cx))?;
 127            let output = this
 128                .overwrite_with_chunks_internal(buffer, edit_chunks, output_events_tx, cx)
 129                .await;
 130            this.project
 131                .update(cx, |project, cx| project.set_agent_location(None, cx))?;
 132            output
 133        });
 134        (task, output_events_rx)
 135    }
 136
 137    async fn overwrite_with_chunks_internal(
 138        &self,
 139        buffer: Entity<Buffer>,
 140        edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
 141        output_events_tx: mpsc::UnboundedSender<EditAgentOutputEvent>,
 142        cx: &mut AsyncApp,
 143    ) -> Result<EditAgentOutput> {
 144        cx.update(|cx| {
 145            buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 146            self.action_log.update(cx, |log, cx| {
 147                log.buffer_edited(buffer.clone(), cx);
 148            });
 149            self.project.update(cx, |project, cx| {
 150                project.set_agent_location(
 151                    Some(AgentLocation {
 152                        buffer: buffer.downgrade(),
 153                        position: language::Anchor::MAX,
 154                    }),
 155                    cx,
 156                )
 157            });
 158            output_events_tx
 159                .unbounded_send(EditAgentOutputEvent::Edited)
 160                .ok();
 161        })?;
 162
 163        let mut raw_edits = String::new();
 164        pin_mut!(edit_chunks);
 165        while let Some(chunk) = edit_chunks.next().await {
 166            let chunk = chunk?;
 167            raw_edits.push_str(&chunk);
 168            cx.update(|cx| {
 169                buffer.update(cx, |buffer, cx| buffer.append(chunk, cx));
 170                self.action_log
 171                    .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 172                self.project.update(cx, |project, cx| {
 173                    project.set_agent_location(
 174                        Some(AgentLocation {
 175                            buffer: buffer.downgrade(),
 176                            position: language::Anchor::MAX,
 177                        }),
 178                        cx,
 179                    )
 180                });
 181            })?;
 182            output_events_tx
 183                .unbounded_send(EditAgentOutputEvent::Edited)
 184                .ok();
 185        }
 186
 187        Ok(EditAgentOutput {
 188            _raw_edits: raw_edits,
 189            _parser_metrics: EditParserMetrics::default(),
 190        })
 191    }
 192
 193    pub fn edit(
 194        &self,
 195        buffer: Entity<Buffer>,
 196        edit_description: String,
 197        previous_messages: Vec<LanguageModelRequestMessage>,
 198        cx: &mut AsyncApp,
 199    ) -> (
 200        Task<Result<EditAgentOutput>>,
 201        mpsc::UnboundedReceiver<EditAgentOutputEvent>,
 202    ) {
 203        self.project
 204            .update(cx, |project, cx| {
 205                project.set_agent_location(
 206                    Some(AgentLocation {
 207                        buffer: buffer.downgrade(),
 208                        position: language::Anchor::MIN,
 209                    }),
 210                    cx,
 211                );
 212            })
 213            .ok();
 214
 215        let this = self.clone();
 216        let (events_tx, events_rx) = mpsc::unbounded();
 217        let output = cx.spawn(async move |cx| {
 218            let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
 219            let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
 220            let prompt = EditFilePromptTemplate {
 221                path,
 222                edit_description,
 223            }
 224            .render(&this.templates)?;
 225            let edit_chunks = this.request(previous_messages, prompt, cx).await?;
 226
 227            let (output, mut inner_events) = this.apply_edit_chunks(buffer, edit_chunks, cx);
 228            while let Some(event) = inner_events.next().await {
 229                events_tx.unbounded_send(event).ok();
 230            }
 231            output.await
 232        });
 233        (output, events_rx)
 234    }
 235
 236    fn apply_edit_chunks(
 237        &self,
 238        buffer: Entity<Buffer>,
 239        edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
 240        cx: &mut AsyncApp,
 241    ) -> (
 242        Task<Result<EditAgentOutput>>,
 243        mpsc::UnboundedReceiver<EditAgentOutputEvent>,
 244    ) {
 245        let (output_events_tx, output_events_rx) = mpsc::unbounded();
 246        let this = self.clone();
 247        let task = cx.spawn(async move |mut cx| {
 248            this.action_log
 249                .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?;
 250            let output = this
 251                .apply_edit_chunks_internal(buffer, edit_chunks, output_events_tx, &mut cx)
 252                .await;
 253            this.project
 254                .update(cx, |project, cx| project.set_agent_location(None, cx))?;
 255            output
 256        });
 257        (task, output_events_rx)
 258    }
 259
 260    async fn apply_edit_chunks_internal(
 261        &self,
 262        buffer: Entity<Buffer>,
 263        edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
 264        output_events: mpsc::UnboundedSender<EditAgentOutputEvent>,
 265        cx: &mut AsyncApp,
 266    ) -> Result<EditAgentOutput> {
 267        let (output, mut edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
 268        while let Some(edit_event) = edit_events.next().await {
 269            let EditParserEvent::OldText(old_text_query) = edit_event? else {
 270                continue;
 271            };
 272
 273            // Skip edits with an empty old text.
 274            if old_text_query.is_empty() {
 275                continue;
 276            }
 277
 278            let old_text_query = SharedString::from(old_text_query);
 279
 280            let (edits_tx, edits_rx) = mpsc::unbounded();
 281            let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 282            let old_range = cx
 283                .background_spawn({
 284                    let snapshot = snapshot.clone();
 285                    let old_text_query = old_text_query.clone();
 286                    async move { Self::resolve_location(&snapshot, &old_text_query) }
 287                })
 288                .await;
 289            let Some(old_range) = old_range else {
 290                // We couldn't find the old text in the buffer. Report the error.
 291                output_events
 292                    .unbounded_send(EditAgentOutputEvent::OldTextNotFound(old_text_query))
 293                    .ok();
 294                continue;
 295            };
 296
 297            let compute_edits = cx.background_spawn(async move {
 298                let buffer_start_indent =
 299                    snapshot.line_indent_for_row(snapshot.offset_to_point(old_range.start).row);
 300                let old_text_start_indent = old_text_query
 301                    .lines()
 302                    .next()
 303                    .map_or(buffer_start_indent, |line| {
 304                        LineIndent::from_iter(line.chars())
 305                    });
 306                let indent_delta = if buffer_start_indent.tabs > 0 {
 307                    IndentDelta::Tabs(
 308                        buffer_start_indent.tabs as isize - old_text_start_indent.tabs as isize,
 309                    )
 310                } else {
 311                    IndentDelta::Spaces(
 312                        buffer_start_indent.spaces as isize - old_text_start_indent.spaces as isize,
 313                    )
 314                };
 315
 316                let old_text = snapshot
 317                    .text_for_range(old_range.clone())
 318                    .collect::<String>();
 319                let mut diff = StreamingDiff::new(old_text);
 320                let mut edit_start = old_range.start;
 321                let mut new_text_chunks =
 322                    Self::reindent_new_text_chunks(indent_delta, &mut edit_events);
 323                let mut done = false;
 324                while !done {
 325                    let char_operations = if let Some(new_text_chunk) = new_text_chunks.next().await
 326                    {
 327                        diff.push_new(&new_text_chunk?)
 328                    } else {
 329                        done = true;
 330                        mem::take(&mut diff).finish()
 331                    };
 332
 333                    for op in char_operations {
 334                        match op {
 335                            CharOperation::Insert { text } => {
 336                                let edit_start = snapshot.anchor_after(edit_start);
 337                                edits_tx
 338                                    .unbounded_send((edit_start..edit_start, Arc::from(text)))?;
 339                            }
 340                            CharOperation::Delete { bytes } => {
 341                                let edit_end = edit_start + bytes;
 342                                let edit_range = snapshot.anchor_after(edit_start)
 343                                    ..snapshot.anchor_before(edit_end);
 344                                edit_start = edit_end;
 345                                edits_tx.unbounded_send((edit_range, Arc::from("")))?;
 346                            }
 347                            CharOperation::Keep { bytes } => edit_start += bytes,
 348                        }
 349                    }
 350                }
 351
 352                drop(new_text_chunks);
 353                anyhow::Ok(edit_events)
 354            });
 355
 356            // TODO: group all edits into one transaction
 357            let mut edits_rx = edits_rx.ready_chunks(32);
 358            while let Some(edits) = edits_rx.next().await {
 359                if edits.is_empty() {
 360                    continue;
 361                }
 362
 363                // Edit the buffer and report edits to the action log as part of the
 364                // same effect cycle, otherwise the edit will be reported as if the
 365                // user made it.
 366                cx.update(|cx| {
 367                    let max_edit_end = buffer.update(cx, |buffer, cx| {
 368                        buffer.edit(edits.iter().cloned(), None, cx);
 369                        let max_edit_end = buffer
 370                            .summaries_for_anchors::<Point, _>(
 371                                edits.iter().map(|(range, _)| &range.end),
 372                            )
 373                            .max()
 374                            .unwrap();
 375                        buffer.anchor_before(max_edit_end)
 376                    });
 377                    self.action_log
 378                        .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 379                    self.project.update(cx, |project, cx| {
 380                        project.set_agent_location(
 381                            Some(AgentLocation {
 382                                buffer: buffer.downgrade(),
 383                                position: max_edit_end,
 384                            }),
 385                            cx,
 386                        );
 387                    });
 388                })?;
 389                output_events
 390                    .unbounded_send(EditAgentOutputEvent::Edited)
 391                    .ok();
 392            }
 393
 394            edit_events = compute_edits.await?;
 395        }
 396
 397        output.await
 398    }
 399
 400    fn parse_edit_chunks(
 401        chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
 402        cx: &mut AsyncApp,
 403    ) -> (
 404        Task<Result<EditAgentOutput>>,
 405        UnboundedReceiver<Result<EditParserEvent>>,
 406    ) {
 407        let (tx, rx) = mpsc::unbounded();
 408        let output = cx.background_spawn(async move {
 409            pin_mut!(chunks);
 410
 411            let mut parser = EditParser::new();
 412            let mut raw_edits = String::new();
 413            while let Some(chunk) = chunks.next().await {
 414                match chunk {
 415                    Ok(chunk) => {
 416                        raw_edits.push_str(&chunk);
 417                        for event in parser.push(&chunk) {
 418                            tx.unbounded_send(Ok(event))?;
 419                        }
 420                    }
 421                    Err(error) => {
 422                        tx.unbounded_send(Err(error.into()))?;
 423                    }
 424                }
 425            }
 426            Ok(EditAgentOutput {
 427                _raw_edits: raw_edits,
 428                _parser_metrics: parser.finish(),
 429            })
 430        });
 431        (output, rx)
 432    }
 433
 434    fn reindent_new_text_chunks(
 435        delta: IndentDelta,
 436        mut stream: impl Unpin + Stream<Item = Result<EditParserEvent>>,
 437    ) -> impl Stream<Item = Result<String>> {
 438        let mut buffer = String::new();
 439        let mut in_leading_whitespace = true;
 440        let mut done = false;
 441        futures::stream::poll_fn(move |cx| {
 442            while !done {
 443                let (chunk, is_last_chunk) = match stream.poll_next_unpin(cx) {
 444                    Poll::Ready(Some(Ok(EditParserEvent::NewTextChunk { chunk, done }))) => {
 445                        (chunk, done)
 446                    }
 447                    Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
 448                    Poll::Pending => return Poll::Pending,
 449                    _ => return Poll::Ready(None),
 450                };
 451
 452                buffer.push_str(&chunk);
 453
 454                let mut indented_new_text = String::new();
 455                let mut start_ix = 0;
 456                let mut newlines = buffer.match_indices('\n').peekable();
 457                loop {
 458                    let (line_end, is_pending_line) = match newlines.next() {
 459                        Some((ix, _)) => (ix, false),
 460                        None => (buffer.len(), true),
 461                    };
 462                    let line = &buffer[start_ix..line_end];
 463
 464                    if in_leading_whitespace {
 465                        if let Some(non_whitespace_ix) = line.find(|c| delta.character() != c) {
 466                            // We found a non-whitespace character, adjust
 467                            // indentation based on the delta.
 468                            let new_indent_len =
 469                                cmp::max(0, non_whitespace_ix as isize + delta.len()) as usize;
 470                            indented_new_text
 471                                .extend(iter::repeat(delta.character()).take(new_indent_len));
 472                            indented_new_text.push_str(&line[non_whitespace_ix..]);
 473                            in_leading_whitespace = false;
 474                        } else if is_pending_line {
 475                            // We're still in leading whitespace and this line is incomplete.
 476                            // Stop processing until we receive more input.
 477                            break;
 478                        } else {
 479                            // This line is entirely whitespace. Push it without indentation.
 480                            indented_new_text.push_str(line);
 481                        }
 482                    } else {
 483                        indented_new_text.push_str(line);
 484                    }
 485
 486                    if is_pending_line {
 487                        start_ix = line_end;
 488                        break;
 489                    } else {
 490                        in_leading_whitespace = true;
 491                        indented_new_text.push('\n');
 492                        start_ix = line_end + 1;
 493                    }
 494                }
 495                buffer.replace_range(..start_ix, "");
 496
 497                // This was the last chunk, push all the buffered content as-is.
 498                if is_last_chunk {
 499                    indented_new_text.push_str(&buffer);
 500                    buffer.clear();
 501                    done = true;
 502                }
 503
 504                if !indented_new_text.is_empty() {
 505                    return Poll::Ready(Some(Ok(indented_new_text)));
 506                }
 507            }
 508
 509            Poll::Ready(None)
 510        })
 511    }
 512
 513    async fn request(
 514        &self,
 515        mut messages: Vec<LanguageModelRequestMessage>,
 516        prompt: String,
 517        cx: &mut AsyncApp,
 518    ) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
 519        let mut message_content = Vec::new();
 520        if let Some(last_message) = messages.last_mut() {
 521            if last_message.role == Role::Assistant {
 522                last_message
 523                    .content
 524                    .retain(|content| !matches!(content, MessageContent::ToolUse(_)));
 525                if last_message.content.is_empty() {
 526                    messages.pop();
 527                }
 528            }
 529        }
 530        message_content.push(MessageContent::Text(prompt));
 531        messages.push(LanguageModelRequestMessage {
 532            role: Role::User,
 533            content: message_content,
 534            cache: false,
 535        });
 536
 537        let request = LanguageModelRequest {
 538            messages,
 539            ..Default::default()
 540        };
 541        Ok(self.model.stream_completion_text(request, cx).await?.stream)
 542    }
 543
 544    fn resolve_location(buffer: &BufferSnapshot, search_query: &str) -> Option<Range<usize>> {
 545        let range = Self::resolve_location_exact(buffer, search_query)
 546            .or_else(|| Self::resolve_location_fuzzy(buffer, search_query))?;
 547
 548        // Expand the range to include entire lines.
 549        let mut start = buffer.offset_to_point(buffer.clip_offset(range.start, Bias::Left));
 550        start.column = 0;
 551        let mut end = buffer.offset_to_point(buffer.clip_offset(range.end, Bias::Right));
 552        if end.column > 0 {
 553            end.column = buffer.line_len(end.row);
 554        }
 555
 556        Some(buffer.point_to_offset(start)..buffer.point_to_offset(end))
 557    }
 558
 559    fn resolve_location_exact(buffer: &BufferSnapshot, search_query: &str) -> Option<Range<usize>> {
 560        let search = AhoCorasick::new([search_query]).ok()?;
 561        let mat = search
 562            .stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
 563            .next()?
 564            .expect("buffer can't error");
 565        Some(mat.range())
 566    }
 567
 568    fn resolve_location_fuzzy(buffer: &BufferSnapshot, search_query: &str) -> Option<Range<usize>> {
 569        const INSERTION_COST: u32 = 3;
 570        const DELETION_COST: u32 = 10;
 571
 572        let buffer_line_count = buffer.max_point().row as usize + 1;
 573        let query_line_count = search_query.lines().count();
 574        let mut matrix = SearchMatrix::new(query_line_count + 1, buffer_line_count + 1);
 575        let mut leading_deletion_cost = 0_u32;
 576        for (row, query_line) in search_query.lines().enumerate() {
 577            let query_line = query_line.trim();
 578            leading_deletion_cost = leading_deletion_cost.saturating_add(DELETION_COST);
 579            matrix.set(
 580                row + 1,
 581                0,
 582                SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
 583            );
 584
 585            let mut buffer_lines = buffer.as_rope().chunks().lines();
 586            let mut col = 0;
 587            while let Some(buffer_line) = buffer_lines.next() {
 588                let buffer_line = buffer_line.trim();
 589                let up = SearchState::new(
 590                    matrix.get(row, col + 1).cost.saturating_add(DELETION_COST),
 591                    SearchDirection::Up,
 592                );
 593                let left = SearchState::new(
 594                    matrix.get(row + 1, col).cost.saturating_add(INSERTION_COST),
 595                    SearchDirection::Left,
 596                );
 597                let diagonal = SearchState::new(
 598                    if fuzzy_eq(query_line, buffer_line) {
 599                        matrix.get(row, col).cost
 600                    } else {
 601                        matrix
 602                            .get(row, col)
 603                            .cost
 604                            .saturating_add(DELETION_COST + INSERTION_COST)
 605                    },
 606                    SearchDirection::Diagonal,
 607                );
 608                matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
 609                col += 1;
 610            }
 611        }
 612
 613        // Traceback to find the best match
 614        let mut buffer_row_end = buffer_line_count as u32;
 615        let mut best_cost = u32::MAX;
 616        for col in 1..=buffer_line_count {
 617            let cost = matrix.get(query_line_count, col).cost;
 618            if cost < best_cost {
 619                best_cost = cost;
 620                buffer_row_end = col as u32;
 621            }
 622        }
 623
 624        let mut matched_lines = 0;
 625        let mut query_row = query_line_count;
 626        let mut buffer_row_start = buffer_row_end;
 627        while query_row > 0 && buffer_row_start > 0 {
 628            let current = matrix.get(query_row, buffer_row_start as usize);
 629            match current.direction {
 630                SearchDirection::Diagonal => {
 631                    query_row -= 1;
 632                    buffer_row_start -= 1;
 633                    matched_lines += 1;
 634                }
 635                SearchDirection::Up => {
 636                    query_row -= 1;
 637                }
 638                SearchDirection::Left => {
 639                    buffer_row_start -= 1;
 640                }
 641            }
 642        }
 643
 644        let matched_buffer_row_count = buffer_row_end - buffer_row_start;
 645        let matched_ratio =
 646            matched_lines as f32 / (matched_buffer_row_count as f32).max(query_line_count as f32);
 647        if matched_ratio >= 0.8 {
 648            let buffer_start_ix = buffer.point_to_offset(Point::new(buffer_row_start, 0));
 649            let buffer_end_ix = buffer.point_to_offset(Point::new(
 650                buffer_row_end - 1,
 651                buffer.line_len(buffer_row_end - 1),
 652            ));
 653            Some(buffer_start_ix..buffer_end_ix)
 654        } else {
 655            None
 656        }
 657    }
 658}
 659
 660fn fuzzy_eq(left: &str, right: &str) -> bool {
 661    const THRESHOLD: f64 = 0.8;
 662
 663    let min_levenshtein = left.len().abs_diff(right.len());
 664    let min_normalized_levenshtein =
 665        1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64);
 666    if min_normalized_levenshtein < THRESHOLD {
 667        return false;
 668    }
 669
 670    strsim::normalized_levenshtein(left, right) >= THRESHOLD
 671}
 672
 673#[derive(Copy, Clone, Debug)]
 674enum IndentDelta {
 675    Spaces(isize),
 676    Tabs(isize),
 677}
 678
 679impl IndentDelta {
 680    fn character(&self) -> char {
 681        match self {
 682            IndentDelta::Spaces(_) => ' ',
 683            IndentDelta::Tabs(_) => '\t',
 684        }
 685    }
 686
 687    fn len(&self) -> isize {
 688        match self {
 689            IndentDelta::Spaces(n) => *n,
 690            IndentDelta::Tabs(n) => *n,
 691        }
 692    }
 693}
 694
 695#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 696enum SearchDirection {
 697    Up,
 698    Left,
 699    Diagonal,
 700}
 701
 702#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
 703struct SearchState {
 704    cost: u32,
 705    direction: SearchDirection,
 706}
 707
 708impl SearchState {
 709    fn new(cost: u32, direction: SearchDirection) -> Self {
 710        Self { cost, direction }
 711    }
 712}
 713
 714struct SearchMatrix {
 715    cols: usize,
 716    data: Vec<SearchState>,
 717}
 718
 719impl SearchMatrix {
 720    fn new(rows: usize, cols: usize) -> Self {
 721        SearchMatrix {
 722            cols,
 723            data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
 724        }
 725    }
 726
 727    fn get(&self, row: usize, col: usize) -> SearchState {
 728        self.data[row * self.cols + col]
 729    }
 730
 731    fn set(&mut self, row: usize, col: usize, cost: SearchState) {
 732        self.data[row * self.cols + col] = cost;
 733    }
 734}
 735
 736#[cfg(test)]
 737mod tests {
 738    use super::*;
 739    use fs::FakeFs;
 740    use futures::stream;
 741    use gpui::{App, AppContext, TestAppContext};
 742    use indoc::indoc;
 743    use language_model::fake_provider::FakeLanguageModel;
 744    use project::{AgentLocation, Project};
 745    use rand::prelude::*;
 746    use rand::rngs::StdRng;
 747    use std::cmp;
 748    use unindent::Unindent;
 749    use util::test::{generate_marked_text, marked_text_ranges};
 750
 751    #[gpui::test(iterations = 100)]
 752    async fn test_empty_old_text(cx: &mut TestAppContext, mut rng: StdRng) {
 753        let agent = init_test(cx).await;
 754        let buffer = cx.new(|cx| {
 755            Buffer::local(
 756                indoc! {"
 757                    abc
 758                    def
 759                    ghi
 760                "},
 761                cx,
 762            )
 763        });
 764        let raw_edits = simulate_llm_output(
 765            indoc! {"
 766                <old_text></old_text>
 767                <new_text>jkl</new_text>
 768                <old_text>def</old_text>
 769                <new_text>DEF</new_text>
 770            "},
 771            &mut rng,
 772            cx,
 773        );
 774        let (apply, _events) =
 775            agent.apply_edit_chunks(buffer.clone(), raw_edits, &mut cx.to_async());
 776        apply.await.unwrap();
 777        pretty_assertions::assert_eq!(
 778            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 779            indoc! {"
 780                abc
 781                DEF
 782                ghi
 783            "}
 784        );
 785    }
 786
 787    #[gpui::test(iterations = 100)]
 788    async fn test_indentation(cx: &mut TestAppContext, mut rng: StdRng) {
 789        let agent = init_test(cx).await;
 790        let buffer = cx.new(|cx| {
 791            Buffer::local(
 792                indoc! {"
 793                    lorem
 794                            ipsum
 795                            dolor
 796                            sit
 797                "},
 798                cx,
 799            )
 800        });
 801        let raw_edits = simulate_llm_output(
 802            indoc! {"
 803                <old_text>
 804                    ipsum
 805                    dolor
 806                    sit
 807                </old_text>
 808                <new_text>
 809                    ipsum
 810                    dolor
 811                    sit
 812                amet
 813                </new_text>
 814            "},
 815            &mut rng,
 816            cx,
 817        );
 818        let (apply, _events) =
 819            agent.apply_edit_chunks(buffer.clone(), raw_edits, &mut cx.to_async());
 820        apply.await.unwrap();
 821        pretty_assertions::assert_eq!(
 822            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 823            indoc! {"
 824                lorem
 825                        ipsum
 826                        dolor
 827                        sit
 828                    amet
 829            "}
 830        );
 831    }
 832
 833    #[gpui::test(iterations = 100)]
 834    async fn test_dependent_edits(cx: &mut TestAppContext, mut rng: StdRng) {
 835        let agent = init_test(cx).await;
 836        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
 837        let raw_edits = simulate_llm_output(
 838            indoc! {"
 839                <old_text>
 840                def
 841                </old_text>
 842                <new_text>
 843                DEF
 844                </new_text>
 845
 846                <old_text>
 847                DEF
 848                </old_text>
 849                <new_text>
 850                DeF
 851                </new_text>
 852            "},
 853            &mut rng,
 854            cx,
 855        );
 856        let (apply, _events) =
 857            agent.apply_edit_chunks(buffer.clone(), raw_edits, &mut cx.to_async());
 858        apply.await.unwrap();
 859        assert_eq!(
 860            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 861            "abc\nDeF\nghi"
 862        );
 863    }
 864
 865    #[gpui::test(iterations = 100)]
 866    async fn test_old_text_hallucination(cx: &mut TestAppContext, mut rng: StdRng) {
 867        let agent = init_test(cx).await;
 868        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
 869        let raw_edits = simulate_llm_output(
 870            indoc! {"
 871                <old_text>
 872                jkl
 873                </old_text>
 874                <new_text>
 875                mno
 876                </new_text>
 877
 878                <old_text>
 879                abc
 880                </old_text>
 881                <new_text>
 882                ABC
 883                </new_text>
 884            "},
 885            &mut rng,
 886            cx,
 887        );
 888        let (apply, _events) =
 889            agent.apply_edit_chunks(buffer.clone(), raw_edits, &mut cx.to_async());
 890        apply.await.unwrap();
 891        assert_eq!(
 892            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 893            "ABC\ndef\nghi"
 894        );
 895    }
 896
 897    #[gpui::test]
 898    async fn test_edit_events(cx: &mut TestAppContext) {
 899        let agent = init_test(cx).await;
 900        let project = agent
 901            .action_log
 902            .read_with(cx, |log, _| log.project().clone());
 903        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
 904        let (chunks_tx, chunks_rx) = mpsc::unbounded();
 905        let (apply, mut events) = agent.apply_edit_chunks(
 906            buffer.clone(),
 907            chunks_rx.map(|chunk: &str| Ok(chunk.to_string())),
 908            &mut cx.to_async(),
 909        );
 910
 911        chunks_tx.unbounded_send("<old_text>a").unwrap();
 912        cx.run_until_parked();
 913        assert_eq!(drain_events(&mut events), vec![]);
 914        assert_eq!(
 915            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 916            "abc\ndef\nghi"
 917        );
 918        assert_eq!(
 919            project.read_with(cx, |project, _| project.agent_location()),
 920            None
 921        );
 922
 923        chunks_tx.unbounded_send("bc</old_text>").unwrap();
 924        cx.run_until_parked();
 925        assert_eq!(drain_events(&mut events), vec![]);
 926        assert_eq!(
 927            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 928            "abc\ndef\nghi"
 929        );
 930        assert_eq!(
 931            project.read_with(cx, |project, _| project.agent_location()),
 932            None
 933        );
 934
 935        chunks_tx.unbounded_send("<new_text>abX").unwrap();
 936        cx.run_until_parked();
 937        assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
 938        assert_eq!(
 939            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 940            "abXc\ndef\nghi"
 941        );
 942        assert_eq!(
 943            project.read_with(cx, |project, _| project.agent_location()),
 944            Some(AgentLocation {
 945                buffer: buffer.downgrade(),
 946                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3)))
 947            })
 948        );
 949
 950        chunks_tx.unbounded_send("cY").unwrap();
 951        cx.run_until_parked();
 952        assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
 953        assert_eq!(
 954            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 955            "abXcY\ndef\nghi"
 956        );
 957        assert_eq!(
 958            project.read_with(cx, |project, _| project.agent_location()),
 959            Some(AgentLocation {
 960                buffer: buffer.downgrade(),
 961                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
 962            })
 963        );
 964
 965        chunks_tx.unbounded_send("</new_text>").unwrap();
 966        chunks_tx.unbounded_send("<old_text>hall").unwrap();
 967        cx.run_until_parked();
 968        assert_eq!(drain_events(&mut events), vec![]);
 969        assert_eq!(
 970            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 971            "abXcY\ndef\nghi"
 972        );
 973        assert_eq!(
 974            project.read_with(cx, |project, _| project.agent_location()),
 975            Some(AgentLocation {
 976                buffer: buffer.downgrade(),
 977                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
 978            })
 979        );
 980
 981        chunks_tx.unbounded_send("ucinated old</old_text>").unwrap();
 982        chunks_tx.unbounded_send("<new_text>").unwrap();
 983        cx.run_until_parked();
 984        assert_eq!(
 985            drain_events(&mut events),
 986            vec![EditAgentOutputEvent::OldTextNotFound(
 987                "hallucinated old".into()
 988            )]
 989        );
 990        assert_eq!(
 991            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
 992            "abXcY\ndef\nghi"
 993        );
 994        assert_eq!(
 995            project.read_with(cx, |project, _| project.agent_location()),
 996            Some(AgentLocation {
 997                buffer: buffer.downgrade(),
 998                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
 999            })
1000        );
1001
1002        chunks_tx.unbounded_send("hallucinated new</new_").unwrap();
1003        chunks_tx.unbounded_send("text>").unwrap();
1004        cx.run_until_parked();
1005        assert_eq!(drain_events(&mut events), vec![]);
1006        assert_eq!(
1007            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1008            "abXcY\ndef\nghi"
1009        );
1010        assert_eq!(
1011            project.read_with(cx, |project, _| project.agent_location()),
1012            Some(AgentLocation {
1013                buffer: buffer.downgrade(),
1014                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
1015            })
1016        );
1017
1018        chunks_tx.unbounded_send("<old_text>gh").unwrap();
1019        chunks_tx.unbounded_send("i</old_text>").unwrap();
1020        chunks_tx.unbounded_send("<new_text>").unwrap();
1021        cx.run_until_parked();
1022        assert_eq!(drain_events(&mut events), vec![]);
1023        assert_eq!(
1024            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1025            "abXcY\ndef\nghi"
1026        );
1027        assert_eq!(
1028            project.read_with(cx, |project, _| project.agent_location()),
1029            Some(AgentLocation {
1030                buffer: buffer.downgrade(),
1031                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
1032            })
1033        );
1034
1035        chunks_tx.unbounded_send("GHI</new_text>").unwrap();
1036        cx.run_until_parked();
1037        assert_eq!(
1038            drain_events(&mut events),
1039            vec![EditAgentOutputEvent::Edited]
1040        );
1041        assert_eq!(
1042            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1043            "abXcY\ndef\nGHI"
1044        );
1045        assert_eq!(
1046            project.read_with(cx, |project, _| project.agent_location()),
1047            Some(AgentLocation {
1048                buffer: buffer.downgrade(),
1049                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3)))
1050            })
1051        );
1052
1053        drop(chunks_tx);
1054        apply.await.unwrap();
1055        assert_eq!(
1056            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1057            "abXcY\ndef\nGHI"
1058        );
1059        assert_eq!(drain_events(&mut events), vec![]);
1060        assert_eq!(
1061            project.read_with(cx, |project, _| project.agent_location()),
1062            None
1063        );
1064    }
1065
1066    #[gpui::test]
1067    async fn test_overwrite_events(cx: &mut TestAppContext) {
1068        let agent = init_test(cx).await;
1069        let project = agent
1070            .action_log
1071            .read_with(cx, |log, _| log.project().clone());
1072        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
1073        let (chunks_tx, chunks_rx) = mpsc::unbounded();
1074        let (apply, mut events) = agent.overwrite_with_chunks(
1075            buffer.clone(),
1076            chunks_rx.map(|chunk: &str| Ok(chunk.to_string())),
1077            &mut cx.to_async(),
1078        );
1079
1080        cx.run_until_parked();
1081        assert_eq!(
1082            drain_events(&mut events),
1083            vec![EditAgentOutputEvent::Edited]
1084        );
1085        assert_eq!(
1086            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1087            ""
1088        );
1089        assert_eq!(
1090            project.read_with(cx, |project, _| project.agent_location()),
1091            Some(AgentLocation {
1092                buffer: buffer.downgrade(),
1093                position: language::Anchor::MAX
1094            })
1095        );
1096
1097        chunks_tx.unbounded_send("jkl\n").unwrap();
1098        cx.run_until_parked();
1099        assert_eq!(
1100            drain_events(&mut events),
1101            vec![EditAgentOutputEvent::Edited]
1102        );
1103        assert_eq!(
1104            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1105            "jkl\n"
1106        );
1107        assert_eq!(
1108            project.read_with(cx, |project, _| project.agent_location()),
1109            Some(AgentLocation {
1110                buffer: buffer.downgrade(),
1111                position: language::Anchor::MAX
1112            })
1113        );
1114
1115        chunks_tx.unbounded_send("mno\n").unwrap();
1116        cx.run_until_parked();
1117        assert_eq!(
1118            drain_events(&mut events),
1119            vec![EditAgentOutputEvent::Edited]
1120        );
1121        assert_eq!(
1122            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1123            "jkl\nmno\n"
1124        );
1125        assert_eq!(
1126            project.read_with(cx, |project, _| project.agent_location()),
1127            Some(AgentLocation {
1128                buffer: buffer.downgrade(),
1129                position: language::Anchor::MAX
1130            })
1131        );
1132
1133        chunks_tx.unbounded_send("pqr").unwrap();
1134        cx.run_until_parked();
1135        assert_eq!(
1136            drain_events(&mut events),
1137            vec![EditAgentOutputEvent::Edited]
1138        );
1139        assert_eq!(
1140            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1141            "jkl\nmno\npqr"
1142        );
1143        assert_eq!(
1144            project.read_with(cx, |project, _| project.agent_location()),
1145            Some(AgentLocation {
1146                buffer: buffer.downgrade(),
1147                position: language::Anchor::MAX
1148            })
1149        );
1150
1151        drop(chunks_tx);
1152        apply.await.unwrap();
1153        assert_eq!(
1154            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
1155            "jkl\nmno\npqr"
1156        );
1157        assert_eq!(drain_events(&mut events), vec![]);
1158        assert_eq!(
1159            project.read_with(cx, |project, _| project.agent_location()),
1160            None
1161        );
1162    }
1163
1164    #[gpui::test]
1165    fn test_resolve_location(cx: &mut App) {
1166        assert_location_resolution(
1167            concat!(
1168                "    Lorem\n",
1169                "«    ipsum»\n",
1170                "    dolor sit amet\n",
1171                "    consecteur",
1172            ),
1173            "ipsum",
1174            cx,
1175        );
1176
1177        assert_location_resolution(
1178            concat!(
1179                "    Lorem\n",
1180                "«    ipsum\n",
1181                "    dolor sit amet»\n",
1182                "    consecteur",
1183            ),
1184            "ipsum\ndolor sit amet",
1185            cx,
1186        );
1187
1188        assert_location_resolution(
1189            &"
1190            «fn foo1(a: usize) -> usize {
1191                40
11921193
1194            fn foo2(b: usize) -> usize {
1195                42
1196            }
1197            "
1198            .unindent(),
1199            "fn foo1(a: usize) -> u32 {\n40\n}",
1200            cx,
1201        );
1202
1203        assert_location_resolution(
1204            &"
1205            class Something {
1206                one() { return 1; }
1207            «    two() { return 2222; }
1208                three() { return 333; }
1209                four() { return 4444; }
1210                five() { return 5555; }
1211                six() { return 6666; }»
1212                seven() { return 7; }
1213                eight() { return 8; }
1214            }
1215            "
1216            .unindent(),
1217            &"
1218                two() { return 2222; }
1219                four() { return 4444; }
1220                five() { return 5555; }
1221                six() { return 6666; }
1222            "
1223            .unindent(),
1224            cx,
1225        );
1226
1227        assert_location_resolution(
1228            &"
1229                use std::ops::Range;
1230                use std::sync::Mutex;
1231                use std::{
1232                    collections::HashMap,
1233                    env,
1234                    ffi::{OsStr, OsString},
1235                    fs,
1236                    io::{BufRead, BufReader},
1237                    mem,
1238                    path::{Path, PathBuf},
1239                    process::Command,
1240                    sync::LazyLock,
1241                    time::SystemTime,
1242                };
1243            "
1244            .unindent(),
1245            &"
1246                use std::collections::{HashMap, HashSet};
1247                use std::ffi::{OsStr, OsString};
1248                use std::fmt::Write as _;
1249                use std::fs;
1250                use std::io::{BufReader, Read, Write};
1251                use std::mem;
1252                use std::path::{Path, PathBuf};
1253                use std::process::Command;
1254                use std::sync::Arc;
1255            "
1256            .unindent(),
1257            cx,
1258        );
1259
1260        assert_location_resolution(
1261            indoc! {"
1262                impl Foo {
1263                    fn new() -> Self {
1264                        Self {
1265                            subscriptions: vec![
1266                                cx.observe_window_activation(window, |editor, window, cx| {
1267                                    let active = window.is_window_active();
1268                                    editor.blink_manager.update(cx, |blink_manager, cx| {
1269                                        if active {
1270                                            blink_manager.enable(cx);
1271                                        } else {
1272                                            blink_manager.disable(cx);
1273                                        }
1274                                    });
1275                                }),
1276                            ];
1277                        }
1278                    }
1279                }
1280            "},
1281            concat!(
1282                "                    editor.blink_manager.update(cx, |blink_manager, cx| {\n",
1283                "                        blink_manager.enable(cx);\n",
1284                "                    });",
1285            ),
1286            cx,
1287        );
1288
1289        assert_location_resolution(
1290            indoc! {r#"
1291                let tool = cx
1292                    .update(|cx| working_set.tool(&tool_name, cx))
1293                    .map_err(|err| {
1294                        anyhow!("Failed to look up tool '{}': {}", tool_name, err)
1295                    })?;
1296
1297                let Some(tool) = tool else {
1298                    return Err(anyhow!("Tool '{}' not found", tool_name));
1299                };
1300
1301                let project = project.clone();
1302                let action_log = action_log.clone();
1303                let messages = messages.clone();
1304                let tool_result = cx
1305                    .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
1306                    .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
1307
1308                tasks.push(tool_result.output);
1309            "#},
1310            concat!(
1311                "let tool_result = cx\n",
1312                "    .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n",
1313                "    .output;",
1314            ),
1315            cx,
1316        );
1317    }
1318
1319    #[gpui::test(iterations = 100)]
1320    async fn test_indent_new_text_chunks(mut rng: StdRng) {
1321        let chunks = to_random_chunks(&mut rng, "    abc\n  def\n      ghi");
1322        let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
1323            Ok(EditParserEvent::NewTextChunk {
1324                chunk: chunk.clone(),
1325                done: index == chunks.len() - 1,
1326            })
1327        }));
1328        let indented_chunks =
1329            EditAgent::reindent_new_text_chunks(IndentDelta::Spaces(2), new_text_chunks)
1330                .collect::<Vec<_>>()
1331                .await;
1332        let new_text = indented_chunks
1333            .into_iter()
1334            .collect::<Result<String>>()
1335            .unwrap();
1336        assert_eq!(new_text, "      abc\n    def\n        ghi");
1337    }
1338
1339    #[gpui::test(iterations = 100)]
1340    async fn test_outdent_new_text_chunks(mut rng: StdRng) {
1341        let chunks = to_random_chunks(&mut rng, "\t\t\t\tabc\n\t\tdef\n\t\t\t\t\t\tghi");
1342        let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
1343            Ok(EditParserEvent::NewTextChunk {
1344                chunk: chunk.clone(),
1345                done: index == chunks.len() - 1,
1346            })
1347        }));
1348        let indented_chunks =
1349            EditAgent::reindent_new_text_chunks(IndentDelta::Tabs(-2), new_text_chunks)
1350                .collect::<Vec<_>>()
1351                .await;
1352        let new_text = indented_chunks
1353            .into_iter()
1354            .collect::<Result<String>>()
1355            .unwrap();
1356        assert_eq!(new_text, "\t\tabc\ndef\n\t\t\t\tghi");
1357    }
1358
1359    #[gpui::test(iterations = 100)]
1360    async fn test_random_indents(mut rng: StdRng) {
1361        let len = rng.gen_range(1..=100);
1362        let new_text = util::RandomCharIter::new(&mut rng)
1363            .with_simple_text()
1364            .take(len)
1365            .collect::<String>();
1366        let new_text = new_text
1367            .split('\n')
1368            .map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
1369            .collect::<Vec<_>>()
1370            .join("\n");
1371        let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
1372
1373        let chunks = to_random_chunks(&mut rng, &new_text);
1374        let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
1375            Ok(EditParserEvent::NewTextChunk {
1376                chunk: chunk.clone(),
1377                done: index == chunks.len() - 1,
1378            })
1379        }));
1380        let reindented_chunks = EditAgent::reindent_new_text_chunks(delta, new_text_chunks)
1381            .collect::<Vec<_>>()
1382            .await;
1383        let actual_reindented_text = reindented_chunks
1384            .into_iter()
1385            .collect::<Result<String>>()
1386            .unwrap();
1387        let expected_reindented_text = new_text
1388            .split('\n')
1389            .map(|line| {
1390                if let Some(ix) = line.find(|c| c != ' ') {
1391                    let new_indent = cmp::max(0, ix as isize + delta.len()) as usize;
1392                    format!("{}{}", " ".repeat(new_indent), &line[ix..])
1393                } else {
1394                    line.to_string()
1395                }
1396            })
1397            .collect::<Vec<_>>()
1398            .join("\n");
1399        assert_eq!(actual_reindented_text, expected_reindented_text);
1400    }
1401
1402    #[track_caller]
1403    fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
1404        let (text, _) = marked_text_ranges(text_with_expected_range, false);
1405        let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
1406        let snapshot = buffer.read(cx).snapshot();
1407        let mut ranges = Vec::new();
1408        ranges.extend(EditAgent::resolve_location(&snapshot, query));
1409        let text_with_actual_range = generate_marked_text(&text, &ranges, false);
1410        pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
1411    }
1412
1413    fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
1414        let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
1415        let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
1416        chunk_indices.sort();
1417        chunk_indices.push(input.len());
1418
1419        let mut chunks = Vec::new();
1420        let mut last_ix = 0;
1421        for chunk_ix in chunk_indices {
1422            chunks.push(input[last_ix..chunk_ix].to_string());
1423            last_ix = chunk_ix;
1424        }
1425        chunks
1426    }
1427
1428    fn simulate_llm_output(
1429        output: &str,
1430        rng: &mut StdRng,
1431        cx: &mut TestAppContext,
1432    ) -> impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>> {
1433        let executor = cx.executor();
1434        stream::iter(to_random_chunks(rng, output).into_iter().map(Ok)).then(move |chunk| {
1435            let executor = executor.clone();
1436            async move {
1437                executor.simulate_random_delay().await;
1438                chunk
1439            }
1440        })
1441    }
1442
1443    async fn init_test(cx: &mut TestAppContext) -> EditAgent {
1444        cx.update(settings::init);
1445        cx.update(Project::init_settings);
1446        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
1447        let model = Arc::new(FakeLanguageModel::default());
1448        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1449        EditAgent::new(model, project, action_log, Templates::new())
1450    }
1451
1452    fn drain_events(
1453        stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
1454    ) -> Vec<EditAgentOutputEvent> {
1455        let mut events = Vec::new();
1456        while let Ok(Some(event)) = stream.try_next() {
1457            events.push(event);
1458        }
1459        events
1460    }
1461}