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
1192 }»
1193
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}