Skip to repository content930 lines · 30.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:58:27.317Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
udiff.rs
1use std::{mem, ops::Range, path::Path, path::PathBuf, sync::Arc};
2
3use anyhow::{Context as _, Result, anyhow};
4use collections::{HashMap, hash_map::Entry};
5use edit_prediction_types::PredictedCursorPosition;
6use gpui::{AsyncApp, Entity};
7use language::{
8 Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, TextBufferSnapshot, ToOffset as _,
9 text_diff,
10};
11use postage::stream::Stream as _;
12use project::Project;
13use util::{paths::PathStyle, rel_path::RelPath};
14use worktree::Worktree;
15use zeta_prompt::udiff::{
16 DiffEvent, DiffParser, FileStatus, Hunk, INLINE_CURSOR_MARKER, disambiguate_by_line_number,
17 find_context_candidates,
18};
19
20pub use zeta_prompt::udiff::{
21 DiffLine, HunkLocation, apply_diff_to_string, apply_diff_to_string_with_hunk_offset,
22 strip_diff_metadata, strip_diff_path_prefix,
23};
24
25#[derive(Clone, Debug)]
26pub struct OpenedBuffers(HashMap<String, Entity<Buffer>>);
27
28impl OpenedBuffers {
29 pub fn get(&self, path: &str) -> Option<&Entity<Buffer>> {
30 self.0.get(path)
31 }
32
33 pub fn buffers(&self) -> impl Iterator<Item = &Entity<Buffer>> {
34 self.0.values()
35 }
36}
37
38pub async fn prediction_edits_for_single_file_diff(
39 diff_str: &str,
40 project: &Entity<Project>,
41 cx: &mut AsyncApp,
42) -> Result<
43 Option<(
44 Entity<Buffer>,
45 BufferSnapshot,
46 Vec<(Range<Anchor>, Arc<str>)>,
47 Option<PredictedCursorPosition>,
48 )>,
49> {
50 let mut diff = DiffParser::new(diff_str);
51 let mut target_file = None;
52 let mut edits = Vec::new();
53 let mut cursor_position = None;
54
55 while let Some(event) = diff.next()? {
56 match event {
57 DiffEvent::Hunk { path, hunk, status } => {
58 anyhow::ensure!(
59 status == FileStatus::Modified,
60 "V4 edit predictions only support modifying existing files"
61 );
62
63 let path = path.to_string();
64 if let Some((target_path, _, _)) = &target_file {
65 anyhow::ensure!(
66 target_path == &path,
67 "V4 edit predictions only support one file"
68 );
69 } else {
70 let project_path = project
71 .update(cx, |project, cx| {
72 project.find_project_path(Path::new(&path), cx)
73 })
74 .with_context(|| format!("no such path: {path}"))?;
75 let buffer = project
76 .update(cx, |project, cx| project.open_buffer(project_path, cx))
77 .await?;
78 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
79
80 target_file = Some((path, buffer, snapshot));
81 }
82
83 let (_, _, snapshot) = target_file.as_ref().context("missing target file")?;
84 let mut pending_marker: Option<(Range<Anchor>, String, usize)> = None;
85 for (range, text) in resolve_hunk_edits_in_buffer(
86 hunk,
87 snapshot,
88 &[Anchor::min_max_range_for_buffer(snapshot.remote_id())],
89 status,
90 )? {
91 let mut remaining = text.as_ref();
92 let mut output = String::new();
93
94 if let Some((pending_range, mut pending_text, pending_offset)) =
95 pending_marker.take()
96 {
97 let matched_len = INLINE_CURSOR_MARKER[pending_text.len()..]
98 .bytes()
99 .zip(remaining.bytes())
100 .take_while(|(left, right)| left == right)
101 .count();
102
103 if matched_len == 0 {
104 edits.push((pending_range, pending_text.into()));
105 } else {
106 let marker_len = pending_text.len() + matched_len;
107 if marker_len == INLINE_CURSOR_MARKER.len() {
108 cursor_position.get_or_insert_with(|| {
109 PredictedCursorPosition::new(
110 pending_range.start,
111 pending_offset,
112 )
113 });
114 remaining = &remaining[matched_len..];
115 } else if matched_len == remaining.len() {
116 pending_text.push_str(
117 &INLINE_CURSOR_MARKER[pending_text.len()..marker_len],
118 );
119 pending_marker =
120 Some((pending_range, pending_text, pending_offset));
121 continue;
122 } else {
123 pending_text.push_str(&remaining[..matched_len]);
124 edits.push((pending_range, pending_text.into()));
125 remaining = &remaining[matched_len..];
126 }
127 }
128 }
129
130 while let Some(marker_offset) = remaining.find(INLINE_CURSOR_MARKER) {
131 output.push_str(&remaining[..marker_offset]);
132 cursor_position.get_or_insert_with(|| {
133 PredictedCursorPosition::new(range.start, output.len())
134 });
135 remaining = &remaining[marker_offset + INLINE_CURSOR_MARKER.len()..];
136 }
137
138 let marker_prefix_len = (1..=INLINE_CURSOR_MARKER.len().min(remaining.len()))
139 .rev()
140 .find(|prefix_len| {
141 remaining.ends_with(&INLINE_CURSOR_MARKER[..*prefix_len])
142 });
143 if let Some(marker_prefix_len) = marker_prefix_len {
144 let marker_start = remaining.len() - marker_prefix_len;
145 output.push_str(&remaining[..marker_start]);
146 pending_marker = Some((
147 range.clone(),
148 remaining[marker_start..].to_string(),
149 output.len(),
150 ));
151 } else {
152 output.push_str(remaining);
153 }
154
155 if range.start.to_offset(snapshot) != range.end.to_offset(snapshot)
156 || !output.is_empty()
157 {
158 edits.push((range, output.into()));
159 }
160 }
161 if let Some((range, text, _)) = pending_marker {
162 edits.push((range, text.into()));
163 }
164 }
165 DiffEvent::FileEnd { renamed_to } => {
166 anyhow::ensure!(
167 renamed_to.is_none(),
168 "V4 edit predictions do not support renames"
169 );
170 }
171 }
172 }
173
174 Ok(target_file.map(|(_, buffer, snapshot)| (buffer, snapshot, edits, cursor_position)))
175}
176
177#[must_use]
178pub async fn apply_diff(
179 diff_str: &str,
180 project: &Entity<Project>,
181 cx: &mut AsyncApp,
182) -> Result<OpenedBuffers> {
183 let worktree = project
184 .read_with(cx, |project, cx| project.visible_worktrees(cx).next())
185 .context("project has no worktree")?;
186
187 let paths: Vec<_> = diff_str
188 .lines()
189 .filter_map(|line| {
190 if let DiffLine::OldPath { path } = DiffLine::parse(line) {
191 if path != "/dev/null" {
192 return Some(PathBuf::from(path.as_ref()));
193 }
194 }
195 None
196 })
197 .collect();
198 refresh_worktree_entries(&worktree, paths.iter().map(|p| p.as_path()), cx).await?;
199
200 let mut included_files: HashMap<String, Entity<Buffer>> = HashMap::default();
201
202 let mut diff = DiffParser::new(diff_str);
203 let mut current_file = None;
204 let mut edits: Vec<(std::ops::Range<Anchor>, Arc<str>)> = vec![];
205
206 while let Some(event) = diff.next()? {
207 match event {
208 DiffEvent::Hunk { path, hunk, status } => {
209 if status == FileStatus::Deleted {
210 let delete_task = project.update(cx, |project, cx| {
211 if let Some(path) = project.find_project_path(path.as_ref(), cx) {
212 project.delete_file(path, cx)
213 } else {
214 None
215 }
216 });
217
218 if let Some(delete_task) = delete_task {
219 delete_task.await?;
220 };
221
222 continue;
223 }
224
225 let buffer = match current_file {
226 None => {
227 let buffer = match included_files.entry(path.to_string()) {
228 Entry::Occupied(entry) => entry.get().clone(),
229 Entry::Vacant(entry) => {
230 let buffer: Entity<Buffer> = if status == FileStatus::Created {
231 project
232 .update(cx, |project, cx| {
233 project.create_buffer(None, true, cx)
234 })
235 .await?
236 } else {
237 let project_path = project
238 .update(cx, |project, cx| {
239 project.find_project_path(path.as_ref(), cx)
240 })
241 .with_context(|| format!("no such path: {}", path))?;
242 project
243 .update(cx, |project, cx| {
244 project.open_buffer(project_path, cx)
245 })
246 .await?
247 };
248 entry.insert(buffer.clone());
249 buffer
250 }
251 };
252 current_file = Some(buffer);
253 current_file.as_ref().unwrap()
254 }
255 Some(ref current) => current,
256 };
257
258 buffer.read_with(cx, |buffer, _| {
259 edits.extend(resolve_hunk_edits_in_buffer(
260 hunk,
261 buffer,
262 &[Anchor::min_max_range_for_buffer(buffer.remote_id())],
263 status,
264 )?);
265 anyhow::Ok(())
266 })?;
267 }
268 DiffEvent::FileEnd { renamed_to } => {
269 let buffer = current_file
270 .take()
271 .context("Got a FileEnd event before an Hunk event")?;
272
273 if let Some(renamed_to) = renamed_to {
274 project
275 .update(cx, |project, cx| {
276 let new_project_path = project
277 .find_project_path(Path::new(renamed_to.as_ref()), cx)
278 .with_context(|| {
279 format!("Failed to find worktree for new path: {}", renamed_to)
280 })?;
281
282 let project_file = project::File::from_dyn(buffer.read(cx).file())
283 .expect("Wrong file type");
284
285 anyhow::Ok(project.rename_entry(
286 project_file.entry_id.unwrap(),
287 new_project_path,
288 cx,
289 ))
290 })?
291 .await?;
292 }
293
294 let edits = mem::take(&mut edits);
295 buffer.update(cx, |buffer, cx| {
296 buffer.edit(edits, None, cx);
297 });
298 }
299 }
300 }
301
302 Ok(OpenedBuffers(included_files))
303}
304
305pub async fn refresh_worktree_entries(
306 worktree: &Entity<Worktree>,
307 paths: impl IntoIterator<Item = &Path>,
308 cx: &mut AsyncApp,
309) -> Result<()> {
310 let mut rel_paths = Vec::new();
311 for path in paths {
312 if let Ok(rel_path) = RelPath::new(path, PathStyle::Unix) {
313 rel_paths.push(rel_path.into_arc());
314 }
315
316 let path_without_root: PathBuf = path.components().skip(1).collect();
317 if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Unix) {
318 rel_paths.push(rel_path.into_arc());
319 }
320 }
321
322 if !rel_paths.is_empty() {
323 worktree
324 .update(cx, |worktree, _| {
325 worktree
326 .as_local()
327 .unwrap()
328 .refresh_entries_for_paths(rel_paths)
329 })
330 .recv()
331 .await;
332 }
333
334 Ok(())
335}
336
337/// Returns the individual edits that would be applied by a diff to the given content.
338/// Each edit is a tuple of (byte_range_in_content, replacement_text).
339/// Uses sub-line diffing to find the precise character positions of changes.
340/// Returns an empty vec if the hunk context is not found or is ambiguous.
341pub fn edits_for_diff(content: &str, diff_str: &str) -> Result<Vec<(Range<usize>, String)>> {
342 let mut diff = DiffParser::new(diff_str);
343 let mut result = Vec::new();
344
345 while let Some(event) = diff.next()? {
346 match event {
347 DiffEvent::Hunk {
348 mut hunk,
349 path: _,
350 status: _,
351 } => {
352 if hunk.context.is_empty() {
353 return Ok(Vec::new());
354 }
355
356 let candidates = find_context_candidates(content, &mut hunk);
357
358 let Some(context_offset) =
359 disambiguate_by_line_number(&candidates, hunk.start_line, &|offset| {
360 content[..offset].matches('\n').count() as u32
361 })
362 else {
363 return Ok(Vec::new());
364 };
365
366 // Use sub-line diffing to find precise edit positions
367 for edit in &hunk.edits {
368 let old_text = &content
369 [context_offset + edit.range.start..context_offset + edit.range.end];
370 let edits_within_hunk = text_diff(old_text, &edit.text);
371 for (inner_range, inner_text) in edits_within_hunk {
372 let absolute_start = context_offset + edit.range.start + inner_range.start;
373 let absolute_end = context_offset + edit.range.start + inner_range.end;
374 result.push((absolute_start..absolute_end, inner_text.to_string()));
375 }
376 }
377 }
378 DiffEvent::FileEnd { .. } => {}
379 }
380 }
381
382 Ok(result)
383}
384
385fn resolve_hunk_edits_in_buffer(
386 mut hunk: Hunk,
387 buffer: &TextBufferSnapshot,
388 ranges: &[Range<Anchor>],
389 status: FileStatus,
390) -> Result<Vec<(Range<Anchor>, Arc<str>)>, anyhow::Error> {
391 let context_offset = if status == FileStatus::Created || hunk.context.is_empty() {
392 0
393 } else {
394 let mut candidates: Vec<usize> = Vec::new();
395 for range in ranges {
396 let range = range.to_offset(buffer);
397 let text = buffer.text_for_range(range.clone()).collect::<String>();
398 for ix in find_context_candidates(&text, &mut hunk) {
399 candidates.push(range.start + ix);
400 }
401 }
402
403 disambiguate_by_line_number(&candidates, hunk.start_line, &|offset| {
404 buffer.offset_to_point(offset).row
405 })
406 .ok_or_else(|| {
407 if candidates.is_empty() {
408 anyhow!("Failed to match context:\n\n```\n{}```\n", hunk.context,)
409 } else {
410 anyhow!("Context is not unique enough:\n{}", hunk.context)
411 }
412 })?
413 };
414
415 if let Some(edit) = hunk.edits.iter().find(|edit| edit.range.end > buffer.len()) {
416 return Err(anyhow!("Edit range {:?} exceeds buffer length", edit.range));
417 }
418
419 Ok(hunk
420 .edits
421 .into_iter()
422 .flat_map(move |edit| {
423 let old_text = buffer
424 .text_for_range(context_offset + edit.range.start..context_offset + edit.range.end)
425 .collect::<String>();
426 let edits_within_hunk = language::text_diff(&old_text, &edit.text);
427 edits_within_hunk
428 .into_iter()
429 .map(move |(inner_range, inner_text)| {
430 (
431 buffer.anchor_after(context_offset + edit.range.start + inner_range.start)
432 ..buffer
433 .anchor_before(context_offset + edit.range.start + inner_range.end),
434 inner_text,
435 )
436 })
437 })
438 .collect())
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use gpui::TestAppContext;
445 use indoc::indoc;
446
447 use pretty_assertions::assert_eq;
448 use project::{FakeFs, Project};
449 use serde_json::json;
450 use settings::SettingsStore;
451 use std::path::Path;
452 use util::path;
453
454 #[test]
455 fn test_line_number_disambiguation() {
456 // Test that line numbers from hunk headers are used to disambiguate
457 // when context before the operation appears multiple times
458 let content = indoc! {"
459 repeated line
460 first unique
461 repeated line
462 second unique
463 "};
464
465 // Context "repeated line" appears twice - line number selects first occurrence
466 let diff = indoc! {"
467 --- a/file.txt
468 +++ b/file.txt
469 @@ -1,2 +1,2 @@
470 repeated line
471 -first unique
472 +REPLACED
473 "};
474
475 let result = edits_for_diff(content, diff).unwrap();
476 assert_eq!(result.len(), 1);
477
478 // The edit should replace "first unique" (after first "repeated line\n" at offset 14)
479 let (range, text) = &result[0];
480 assert_eq!(range.start, 14);
481 assert_eq!(range.end, 26); // "first unique" is 12 bytes
482 assert_eq!(text, "REPLACED");
483 }
484
485 #[test]
486 fn test_line_number_disambiguation_second_match() {
487 // Test disambiguation when the edit should apply to a later occurrence
488 let content = indoc! {"
489 repeated line
490 first unique
491 repeated line
492 second unique
493 "};
494
495 // Context "repeated line" appears twice - line number selects second occurrence
496 let diff = indoc! {"
497 --- a/file.txt
498 +++ b/file.txt
499 @@ -3,2 +3,2 @@
500 repeated line
501 -second unique
502 +REPLACED
503 "};
504
505 let result = edits_for_diff(content, diff).unwrap();
506 assert_eq!(result.len(), 1);
507
508 // The edit should replace "second unique" (after second "repeated line\n")
509 // Offset: "repeated line\n" (14) + "first unique\n" (13) + "repeated line\n" (14) = 41
510 let (range, text) = &result[0];
511 assert_eq!(range.start, 41);
512 assert_eq!(range.end, 54); // "second unique" is 13 bytes
513 assert_eq!(text, "REPLACED");
514 }
515
516 #[gpui::test]
517 async fn test_prediction_edits_for_single_file_diff_can_target_project_file(
518 cx: &mut TestAppContext,
519 ) {
520 let fs = init_test(cx);
521 fs.insert_tree(
522 path!("/root"),
523 json!({
524 "file1": "Hello!\nHow\nBye\n",
525 "file2": "Hola!\nComo\nAdios\n",
526 }),
527 )
528 .await;
529 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
530
531 let diff = indoc! {r#"
532 --- a/file2
533 +++ b/file2
534 @@ ... @@
535 Hola!
536 -Como
537 +Como estas?
538 Adios
539 "#};
540
541 let (buffer, snapshot, edits, cursor_position) =
542 prediction_edits_for_single_file_diff(diff, &project, &mut cx.to_async())
543 .await
544 .unwrap()
545 .unwrap();
546
547 assert!(cursor_position.is_none());
548 buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
549 assert_eq!(
550 snapshot.file().unwrap().path().as_std_path(),
551 Path::new("file2")
552 );
553 buffer.read_with(cx, |buffer, _cx| {
554 assert_eq!(buffer.text(), "Hola!\nComo estas?\nAdios\n");
555 });
556 }
557
558 #[gpui::test]
559 async fn test_prediction_edits_for_single_file_diff_strips_inline_cursor_marker(
560 cx: &mut TestAppContext,
561 ) {
562 let fs = init_test(cx);
563 fs.insert_tree(
564 path!("/root"),
565 json!({
566 "file": "Hello!\nHow\nBye\n",
567 }),
568 )
569 .await;
570 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
571
572 let diff = indoc! {r#"
573 --- a/file
574 +++ b/file
575 @@ ... @@
576 Hello!
577 -How
578 +How are <|user_cursor|>you?
579 Bye
580 "#};
581
582 let (buffer, snapshot, edits, cursor_position) =
583 prediction_edits_for_single_file_diff(diff, &project, &mut cx.to_async())
584 .await
585 .unwrap()
586 .unwrap();
587
588 assert!(
589 edits
590 .iter()
591 .all(|(_, text)| !text.contains(INLINE_CURSOR_MARKER))
592 );
593 let cursor_position = cursor_position.unwrap();
594 assert_eq!(
595 cursor_position.anchor.to_offset(&snapshot),
596 "Hello!\nHow".len()
597 );
598 assert_eq!(cursor_position.offset, " are ".len());
599
600 buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
601 buffer.read_with(cx, |buffer, _cx| {
602 assert_eq!(buffer.text(), "Hello!\nHow are you?\nBye\n");
603 });
604 }
605
606 #[gpui::test]
607 async fn test_prediction_edits_for_single_file_diff_drops_marker_only_edit(
608 cx: &mut TestAppContext,
609 ) {
610 let fs = init_test(cx);
611 fs.insert_tree(
612 path!("/root"),
613 json!({
614 "file": "Name</Update>\n",
615 }),
616 )
617 .await;
618 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
619
620 let diff = indoc! {r#"
621 --- a/file
622 +++ b/file
623 @@ ... @@
624 -Name</Update>
625 +<|user_cursor|>Name</Update>
626 "#};
627
628 let (buffer, snapshot, edits, cursor_position) =
629 prediction_edits_for_single_file_diff(diff, &project, &mut cx.to_async())
630 .await
631 .unwrap()
632 .unwrap();
633
634 assert!(edits.is_empty());
635 let cursor_position = cursor_position.unwrap();
636 assert_eq!(cursor_position.anchor.to_offset(&snapshot), 0);
637 assert_eq!(cursor_position.offset, 0);
638
639 buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
640 buffer.read_with(cx, |buffer, _cx| {
641 assert_eq!(buffer.text(), "Name</Update>\n");
642 });
643 }
644
645 #[gpui::test]
646 async fn test_prediction_edits_for_single_file_diff_does_not_treat_completed_literal_marker_as_cursor(
647 cx: &mut TestAppContext,
648 ) {
649 let fs = init_test(cx);
650 fs.insert_tree(
651 path!("/root"),
652 json!({
653 "file": "text <|user_cursor\n",
654 }),
655 )
656 .await;
657 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
658
659 let diff = indoc! {r#"
660 --- a/file
661 +++ b/file
662 @@ ... @@
663 -text <|user_cursor
664 +text <|user_cursor|>
665 "#};
666
667 let (buffer, _, edits, cursor_position) =
668 prediction_edits_for_single_file_diff(diff, &project, &mut cx.to_async())
669 .await
670 .unwrap()
671 .unwrap();
672
673 assert!(cursor_position.is_none());
674 buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
675 buffer.read_with(cx, |buffer, _cx| {
676 assert_eq!(buffer.text(), "text <|user_cursor|>\n");
677 });
678 }
679
680 #[gpui::test]
681 async fn test_apply_diff_successful(cx: &mut TestAppContext) {
682 let fs = init_test(cx);
683
684 let buffer_1_text = indoc! {r#"
685 one
686 two
687 three
688 four
689 five
690 "# };
691
692 let buffer_1_text_final = indoc! {r#"
693 3
694 4
695 5
696 "# };
697
698 let buffer_2_text = indoc! {r#"
699 six
700 seven
701 eight
702 nine
703 ten
704 "# };
705
706 let buffer_2_text_final = indoc! {r#"
707 5
708 six
709 seven
710 7.5
711 eight
712 nine
713 ten
714 11
715 "# };
716
717 fs.insert_tree(
718 path!("/root"),
719 json!({
720 "file1": buffer_1_text,
721 "file2": buffer_2_text,
722 }),
723 )
724 .await;
725
726 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
727
728 let diff = indoc! {r#"
729 --- a/file1
730 +++ b/file1
731 one
732 two
733 -three
734 +3
735 four
736 five
737 --- a/file1
738 +++ b/file1
739 3
740 -four
741 -five
742 +4
743 +5
744 --- a/file1
745 +++ b/file1
746 -one
747 -two
748 3
749 4
750 --- a/file2
751 +++ b/file2
752 +5
753 six
754 --- a/file2
755 +++ b/file2
756 seven
757 +7.5
758 eight
759 --- a/file2
760 +++ b/file2
761 ten
762 +11
763 "#};
764
765 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
766 .await
767 .unwrap();
768 let buffer_1 = project
769 .update(cx, |project, cx| {
770 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
771 project.open_buffer(project_path, cx)
772 })
773 .await
774 .unwrap();
775
776 buffer_1.read_with(cx, |buffer, _cx| {
777 assert_eq!(buffer.text(), buffer_1_text_final);
778 });
779 let buffer_2 = project
780 .update(cx, |project, cx| {
781 let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
782 project.open_buffer(project_path, cx)
783 })
784 .await
785 .unwrap();
786
787 buffer_2.read_with(cx, |buffer, _cx| {
788 assert_eq!(buffer.text(), buffer_2_text_final);
789 });
790 }
791
792 #[gpui::test]
793 async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
794 let fs = init_test(cx);
795
796 let start = indoc! {r#"
797 one
798 two
799 three
800 four
801 five
802
803 four
804 five
805 "# };
806
807 let end = indoc! {r#"
808 one
809 two
810 3
811 four
812 5
813
814 four
815 five
816 "# };
817
818 fs.insert_tree(
819 path!("/root"),
820 json!({
821 "file1": start,
822 }),
823 )
824 .await;
825
826 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
827
828 let diff = indoc! {r#"
829 --- a/file1
830 +++ b/file1
831 one
832 two
833 -three
834 +3
835 four
836 -five
837 +5
838 "#};
839
840 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
841 .await
842 .unwrap();
843
844 let buffer_1 = project
845 .update(cx, |project, cx| {
846 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
847 project.open_buffer(project_path, cx)
848 })
849 .await
850 .unwrap();
851
852 buffer_1.read_with(cx, |buffer, _cx| {
853 assert_eq!(buffer.text(), end);
854 });
855 }
856
857 fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
858 cx.update(|cx| {
859 let settings_store = SettingsStore::test(cx);
860 cx.set_global(settings_store);
861 });
862
863 FakeFs::new(cx.background_executor.clone())
864 }
865
866 #[test]
867 fn test_edits_for_diff() {
868 let content = indoc! {"
869 fn main() {
870 let x = 1;
871 let y = 2;
872 println!(\"{} {}\", x, y);
873 }
874 "};
875
876 let diff = indoc! {"
877 --- a/file.rs
878 +++ b/file.rs
879 @@ -1,5 +1,5 @@
880 fn main() {
881 - let x = 1;
882 + let x = 42;
883 let y = 2;
884 println!(\"{} {}\", x, y);
885 }
886 "};
887
888 let edits = edits_for_diff(content, diff).unwrap();
889 assert_eq!(edits.len(), 1);
890
891 let (range, replacement) = &edits[0];
892 // With sub-line diffing, the edit should start at "1" (the actual changed character)
893 let expected_start = content.find("let x = 1;").unwrap() + "let x = ".len();
894 assert_eq!(range.start, expected_start);
895 // The deleted text is just "1"
896 assert_eq!(range.end, expected_start + "1".len());
897 // The replacement text
898 assert_eq!(replacement, "42");
899
900 // Verify the cursor would be positioned at the column of "1"
901 let line_start = content[..range.start]
902 .rfind('\n')
903 .map(|p| p + 1)
904 .unwrap_or(0);
905 let cursor_column = range.start - line_start;
906 // " let x = " is 12 characters, so column 12
907 assert_eq!(cursor_column, " let x = ".len());
908 }
909
910 #[test]
911 fn test_edits_for_diff_no_trailing_newline() {
912 let content = "foo\nbar\nbaz";
913 let diff = indoc! {"
914 --- a/file.txt
915 +++ b/file.txt
916 @@ -1,3 +1,3 @@
917 foo
918 -bar
919 +qux
920 baz
921 "};
922
923 let result = edits_for_diff(content, diff).unwrap();
924 assert_eq!(result.len(), 1);
925 let (range, text) = &result[0];
926 assert_eq!(&content[range.clone()], "bar");
927 assert_eq!(text, "qux");
928 }
929}
930