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