Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:07:58.628Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

udiff.rs

930 lines · 30.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.