udiff.rs

   1use std::borrow::Cow;
   2use std::fmt::Display;
   3use std::sync::Arc;
   4use std::{
   5    fmt::{Debug, Write},
   6    mem,
   7    ops::Range,
   8    path::Path,
   9};
  10
  11use anyhow::Context as _;
  12use anyhow::Result;
  13use anyhow::anyhow;
  14use collections::HashMap;
  15use gpui::AsyncApp;
  16use gpui::Entity;
  17use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, TextBufferSnapshot};
  18use project::Project;
  19
  20pub async fn parse_diff<'a>(
  21    diff: &'a str,
  22    get_buffer: impl Fn(&Path) -> Option<(&'a BufferSnapshot, &'a [Range<Anchor>])> + Send,
  23) -> Result<(&'a BufferSnapshot, Vec<(Range<Anchor>, Arc<str>)>)> {
  24    let mut diff = DiffParser::new(diff);
  25    let mut edited_buffer = None;
  26    let mut edits = Vec::new();
  27
  28    while let Some(event) = diff.next()? {
  29        match event {
  30            DiffEvent::Hunk {
  31                path: file_path,
  32                hunk,
  33            } => {
  34                let (buffer, ranges) = match edited_buffer {
  35                    None => {
  36                        edited_buffer = get_buffer(&Path::new(file_path.as_ref()));
  37                        edited_buffer
  38                            .as_ref()
  39                            .context("Model tried to edit a file that wasn't included")?
  40                    }
  41                    Some(ref current) => current,
  42                };
  43
  44                edits.extend(resolve_hunk_edits_in_buffer(hunk, &buffer.text, ranges)?);
  45            }
  46            DiffEvent::FileEnd { renamed_to } => {
  47                let (buffer, _) = edited_buffer
  48                    .take()
  49                    .expect("Got a FileEnd event before an Hunk event");
  50
  51                if renamed_to.is_some() {
  52                    anyhow::bail!("edit predictions cannot rename files");
  53                }
  54
  55                if diff.next()?.is_some() {
  56                    anyhow::bail!("Edited more than one file");
  57                }
  58
  59                return Ok((buffer, edits));
  60            }
  61        }
  62    }
  63
  64    Err(anyhow::anyhow!("No EOF"))
  65}
  66
  67#[derive(Debug)]
  68pub struct OpenedBuffers<'a>(#[allow(unused)] HashMap<Cow<'a, str>, Entity<Buffer>>);
  69
  70#[must_use]
  71pub async fn apply_diff<'a>(
  72    diff: &'a str,
  73    project: &Entity<Project>,
  74    cx: &mut AsyncApp,
  75) -> Result<OpenedBuffers<'a>> {
  76    let mut included_files = HashMap::default();
  77
  78    for line in diff.lines() {
  79        let diff_line = DiffLine::parse(line);
  80
  81        if let DiffLine::OldPath { path } = diff_line {
  82            let buffer = project
  83                .update(cx, |project, cx| {
  84                    let project_path =
  85                        project
  86                            .find_project_path(path.as_ref(), cx)
  87                            .with_context(|| {
  88                                format!("Failed to find worktree for new path: {}", path)
  89                            })?;
  90                    anyhow::Ok(project.open_buffer(project_path, cx))
  91                })??
  92                .await?;
  93
  94            included_files.insert(path, buffer);
  95        }
  96    }
  97
  98    let ranges = [Anchor::MIN..Anchor::MAX];
  99
 100    let mut diff = DiffParser::new(diff);
 101    let mut current_file = None;
 102    let mut edits = vec![];
 103
 104    while let Some(event) = diff.next()? {
 105        match event {
 106            DiffEvent::Hunk {
 107                path: file_path,
 108                hunk,
 109            } => {
 110                let (buffer, ranges) = match current_file {
 111                    None => {
 112                        let buffer = included_files
 113                            .get_mut(&file_path)
 114                            .expect("Opened all files in diff");
 115
 116                        current_file = Some((buffer, ranges.as_slice()));
 117                        current_file.as_ref().unwrap()
 118                    }
 119                    Some(ref current) => current,
 120                };
 121
 122                buffer.read_with(cx, |buffer, _| {
 123                    edits.extend(resolve_hunk_edits_in_buffer(hunk, buffer, ranges)?);
 124                    anyhow::Ok(())
 125                })??;
 126            }
 127            DiffEvent::FileEnd { renamed_to } => {
 128                let (buffer, _) = current_file
 129                    .take()
 130                    .expect("Got a FileEnd event before an Hunk event");
 131
 132                if let Some(renamed_to) = renamed_to {
 133                    project
 134                        .update(cx, |project, cx| {
 135                            let new_project_path = project
 136                                .find_project_path(Path::new(renamed_to.as_ref()), cx)
 137                                .with_context(|| {
 138                                    format!("Failed to find worktree for new path: {}", renamed_to)
 139                                })?;
 140
 141                            let project_file = project::File::from_dyn(buffer.read(cx).file())
 142                                .expect("Wrong file type");
 143
 144                            anyhow::Ok(project.rename_entry(
 145                                project_file.entry_id.unwrap(),
 146                                new_project_path,
 147                                cx,
 148                            ))
 149                        })??
 150                        .await?;
 151                }
 152
 153                let edits = mem::take(&mut edits);
 154                buffer.update(cx, |buffer, cx| {
 155                    buffer.edit(edits, None, cx);
 156                })?;
 157            }
 158        }
 159    }
 160
 161    Ok(OpenedBuffers(included_files))
 162}
 163
 164struct PatchFile<'a> {
 165    old_path: Cow<'a, str>,
 166    new_path: Cow<'a, str>,
 167}
 168
 169struct DiffParser<'a> {
 170    current_file: Option<PatchFile<'a>>,
 171    current_line: Option<(&'a str, DiffLine<'a>)>,
 172    hunk: Hunk,
 173    diff: std::str::Lines<'a>,
 174}
 175
 176#[derive(Debug, PartialEq)]
 177enum DiffEvent<'a> {
 178    Hunk { path: Cow<'a, str>, hunk: Hunk },
 179    FileEnd { renamed_to: Option<Cow<'a, str>> },
 180}
 181
 182#[derive(Debug, Default, PartialEq)]
 183struct Hunk {
 184    context: String,
 185    edits: Vec<Edit>,
 186}
 187
 188impl Hunk {
 189    fn is_empty(&self) -> bool {
 190        self.context.is_empty() && self.edits.is_empty()
 191    }
 192}
 193
 194#[derive(Debug, PartialEq)]
 195struct Edit {
 196    range: Range<usize>,
 197    text: String,
 198}
 199
 200impl<'a> DiffParser<'a> {
 201    fn new(diff: &'a str) -> Self {
 202        let mut diff = diff.lines();
 203        let current_line = diff.next().map(|line| (line, DiffLine::parse(line)));
 204        DiffParser {
 205            current_file: None,
 206            hunk: Hunk::default(),
 207            current_line,
 208            diff,
 209        }
 210    }
 211
 212    fn next(&mut self) -> Result<Option<DiffEvent<'a>>> {
 213        loop {
 214            let (hunk_done, file_done) = match self.current_line.as_ref().map(|e| &e.1) {
 215                Some(DiffLine::OldPath { .. }) | Some(DiffLine::Garbage(_)) | None => (true, true),
 216                Some(DiffLine::HunkHeader(_)) => (true, false),
 217                _ => (false, false),
 218            };
 219
 220            if hunk_done {
 221                if let Some(file) = &self.current_file
 222                    && !self.hunk.is_empty()
 223                {
 224                    return Ok(Some(DiffEvent::Hunk {
 225                        path: file.old_path.clone(),
 226                        hunk: mem::take(&mut self.hunk),
 227                    }));
 228                }
 229            }
 230
 231            if file_done {
 232                if let Some(PatchFile { old_path, new_path }) = self.current_file.take() {
 233                    return Ok(Some(DiffEvent::FileEnd {
 234                        renamed_to: if old_path != new_path {
 235                            Some(new_path)
 236                        } else {
 237                            None
 238                        },
 239                    }));
 240                }
 241            }
 242
 243            let Some((line, parsed_line)) = self.current_line.take() else {
 244                break;
 245            };
 246
 247            util::maybe!({
 248                match parsed_line {
 249                    DiffLine::OldPath { path } => {
 250                        self.current_file = Some(PatchFile {
 251                            old_path: path,
 252                            new_path: "".into(),
 253                        });
 254                    }
 255                    DiffLine::NewPath { path } => {
 256                        if let Some(current_file) = &mut self.current_file {
 257                            current_file.new_path = path
 258                        }
 259                    }
 260                    DiffLine::HunkHeader(_) => {}
 261                    DiffLine::Context(ctx) => {
 262                        if self.current_file.is_some() {
 263                            writeln!(&mut self.hunk.context, "{ctx}")?;
 264                        }
 265                    }
 266                    DiffLine::Deletion(del) => {
 267                        if self.current_file.is_some() {
 268                            let range = self.hunk.context.len()
 269                                ..self.hunk.context.len() + del.len() + '\n'.len_utf8();
 270                            if let Some(last_edit) = self.hunk.edits.last_mut()
 271                                && last_edit.range.end == range.start
 272                            {
 273                                last_edit.range.end = range.end;
 274                            } else {
 275                                self.hunk.edits.push(Edit {
 276                                    range,
 277                                    text: String::new(),
 278                                });
 279                            }
 280                            writeln!(&mut self.hunk.context, "{del}")?;
 281                        }
 282                    }
 283                    DiffLine::Addition(add) => {
 284                        if self.current_file.is_some() {
 285                            let range = self.hunk.context.len()..self.hunk.context.len();
 286                            if let Some(last_edit) = self.hunk.edits.last_mut()
 287                                && last_edit.range.end == range.start
 288                            {
 289                                writeln!(&mut last_edit.text, "{add}").unwrap();
 290                            } else {
 291                                self.hunk.edits.push(Edit {
 292                                    range,
 293                                    text: format!("{add}\n"),
 294                                });
 295                            }
 296                        }
 297                    }
 298                    DiffLine::Garbage(_) => {}
 299                }
 300
 301                anyhow::Ok(())
 302            })
 303            .with_context(|| format!("on line:\n\n```\n{}```", line))?;
 304
 305            self.current_line = self.diff.next().map(|line| (line, DiffLine::parse(line)));
 306        }
 307
 308        anyhow::Ok(None)
 309    }
 310}
 311
 312fn resolve_hunk_edits_in_buffer(
 313    hunk: Hunk,
 314    buffer: &TextBufferSnapshot,
 315    ranges: &[Range<Anchor>],
 316) -> Result<impl Iterator<Item = (Range<Anchor>, Arc<str>)>, anyhow::Error> {
 317    let context_offset = if hunk.context.is_empty() {
 318        Ok(0)
 319    } else {
 320        let mut offset = None;
 321        for range in ranges {
 322            let range = range.to_offset(buffer);
 323            let text = buffer.text_for_range(range.clone()).collect::<String>();
 324            for (ix, _) in text.match_indices(&hunk.context) {
 325                if offset.is_some() {
 326                    anyhow::bail!("Context is not unique enough:\n{}", hunk.context);
 327                }
 328                offset = Some(range.start + ix);
 329            }
 330        }
 331        offset.ok_or_else(|| {
 332            anyhow!(
 333                "Failed to match context:\n{}\n\nBuffer:\n{}",
 334                hunk.context,
 335                buffer.text(),
 336            )
 337        })
 338    }?;
 339    let iter = hunk.edits.into_iter().flat_map(move |edit| {
 340        let old_text = buffer
 341            .text_for_range(context_offset + edit.range.start..context_offset + edit.range.end)
 342            .collect::<String>();
 343        let edits_within_hunk = language::text_diff(&old_text, &edit.text);
 344        edits_within_hunk
 345            .into_iter()
 346            .map(move |(inner_range, inner_text)| {
 347                (
 348                    buffer.anchor_after(context_offset + edit.range.start + inner_range.start)
 349                        ..buffer.anchor_before(context_offset + edit.range.start + inner_range.end),
 350                    inner_text,
 351                )
 352            })
 353    });
 354    Ok(iter)
 355}
 356
 357#[derive(Debug, PartialEq)]
 358pub enum DiffLine<'a> {
 359    OldPath { path: Cow<'a, str> },
 360    NewPath { path: Cow<'a, str> },
 361    HunkHeader(Option<HunkLocation>),
 362    Context(&'a str),
 363    Deletion(&'a str),
 364    Addition(&'a str),
 365    Garbage(&'a str),
 366}
 367
 368#[derive(Debug, PartialEq)]
 369pub struct HunkLocation {
 370    start_line_old: u32,
 371    count_old: u32,
 372    start_line_new: u32,
 373    count_new: u32,
 374}
 375
 376impl<'a> DiffLine<'a> {
 377    pub fn parse(line: &'a str) -> Self {
 378        Self::try_parse(line).unwrap_or(Self::Garbage(line))
 379    }
 380
 381    fn try_parse(line: &'a str) -> Option<Self> {
 382        if let Some(header) = line.strip_prefix("---").and_then(eat_required_whitespace) {
 383            let path = parse_header_path("a/", header);
 384            Some(Self::OldPath { path })
 385        } else if let Some(header) = line.strip_prefix("+++").and_then(eat_required_whitespace) {
 386            Some(Self::NewPath {
 387                path: parse_header_path("b/", header),
 388            })
 389        } else if let Some(header) = line.strip_prefix("@@").and_then(eat_required_whitespace) {
 390            if header.starts_with("...") {
 391                return Some(Self::HunkHeader(None));
 392            }
 393
 394            let (start_line_old, header) = header.strip_prefix('-')?.split_once(',')?;
 395            let mut parts = header.split_ascii_whitespace();
 396            let count_old = parts.next()?;
 397            let (start_line_new, count_new) = parts.next()?.strip_prefix('+')?.split_once(',')?;
 398
 399            Some(Self::HunkHeader(Some(HunkLocation {
 400                start_line_old: start_line_old.parse::<u32>().ok()?.saturating_sub(1),
 401                count_old: count_old.parse().ok()?,
 402                start_line_new: start_line_new.parse::<u32>().ok()?.saturating_sub(1),
 403                count_new: count_new.parse().ok()?,
 404            })))
 405        } else if let Some(deleted_header) = line.strip_prefix("-") {
 406            Some(Self::Deletion(deleted_header))
 407        } else if line.is_empty() {
 408            Some(Self::Context(""))
 409        } else if let Some(context) = line.strip_prefix(" ") {
 410            Some(Self::Context(context))
 411        } else {
 412            Some(Self::Addition(line.strip_prefix("+")?))
 413        }
 414    }
 415}
 416
 417impl<'a> Display for DiffLine<'a> {
 418    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 419        match self {
 420            DiffLine::OldPath { path } => write!(f, "--- {path}"),
 421            DiffLine::NewPath { path } => write!(f, "+++ {path}"),
 422            DiffLine::HunkHeader(Some(hunk_location)) => {
 423                write!(
 424                    f,
 425                    "@@ -{},{} +{},{} @@",
 426                    hunk_location.start_line_old + 1,
 427                    hunk_location.count_old,
 428                    hunk_location.start_line_new + 1,
 429                    hunk_location.count_new
 430                )
 431            }
 432            DiffLine::HunkHeader(None) => write!(f, "@@ ... @@"),
 433            DiffLine::Context(content) => write!(f, " {content}"),
 434            DiffLine::Deletion(content) => write!(f, "-{content}"),
 435            DiffLine::Addition(content) => write!(f, "+{content}"),
 436            DiffLine::Garbage(line) => write!(f, "{line}"),
 437        }
 438    }
 439}
 440
 441fn parse_header_path<'a>(strip_prefix: &'static str, header: &'a str) -> Cow<'a, str> {
 442    if !header.contains(['"', '\\']) {
 443        let path = header.split_ascii_whitespace().next().unwrap_or(header);
 444        return Cow::Borrowed(path.strip_prefix(strip_prefix).unwrap_or(path));
 445    }
 446
 447    let mut path = String::with_capacity(header.len());
 448    let mut in_quote = false;
 449    let mut chars = header.chars().peekable();
 450    let mut strip_prefix = Some(strip_prefix);
 451
 452    while let Some(char) = chars.next() {
 453        if char == '"' {
 454            in_quote = !in_quote;
 455        } else if char == '\\' {
 456            let Some(&next_char) = chars.peek() else {
 457                break;
 458            };
 459            chars.next();
 460            path.push(next_char);
 461        } else if char.is_ascii_whitespace() && !in_quote {
 462            break;
 463        } else {
 464            path.push(char);
 465        }
 466
 467        if let Some(prefix) = strip_prefix
 468            && path == prefix
 469        {
 470            strip_prefix.take();
 471            path.clear();
 472        }
 473    }
 474
 475    Cow::Owned(path)
 476}
 477
 478fn eat_required_whitespace(header: &str) -> Option<&str> {
 479    let trimmed = header.trim_ascii_start();
 480
 481    if trimmed.len() == header.len() {
 482        None
 483    } else {
 484        Some(trimmed)
 485    }
 486}
 487
 488#[cfg(test)]
 489mod tests {
 490    use super::*;
 491    use gpui::TestAppContext;
 492    use indoc::indoc;
 493    use language::Point;
 494    use pretty_assertions::assert_eq;
 495    use project::{FakeFs, Project};
 496    use serde_json::json;
 497    use settings::SettingsStore;
 498    use util::path;
 499
 500    #[test]
 501    fn parse_lines_simple() {
 502        let input = indoc! {"
 503            diff --git a/text.txt b/text.txt
 504            index 86c770d..a1fd855 100644
 505            --- a/file.txt
 506            +++ b/file.txt
 507            @@ -1,2 +1,3 @@
 508             context
 509            -deleted
 510            +inserted
 511            garbage
 512
 513            --- b/file.txt
 514            +++ a/file.txt
 515        "};
 516
 517        let lines = input.lines().map(DiffLine::parse).collect::<Vec<_>>();
 518
 519        pretty_assertions::assert_eq!(
 520            lines,
 521            &[
 522                DiffLine::Garbage("diff --git a/text.txt b/text.txt"),
 523                DiffLine::Garbage("index 86c770d..a1fd855 100644"),
 524                DiffLine::OldPath {
 525                    path: "file.txt".into()
 526                },
 527                DiffLine::NewPath {
 528                    path: "file.txt".into()
 529                },
 530                DiffLine::HunkHeader(Some(HunkLocation {
 531                    start_line_old: 0,
 532                    count_old: 2,
 533                    start_line_new: 0,
 534                    count_new: 3
 535                })),
 536                DiffLine::Context("context"),
 537                DiffLine::Deletion("deleted"),
 538                DiffLine::Addition("inserted"),
 539                DiffLine::Garbage("garbage"),
 540                DiffLine::Context(""),
 541                DiffLine::OldPath {
 542                    path: "b/file.txt".into()
 543                },
 544                DiffLine::NewPath {
 545                    path: "a/file.txt".into()
 546                },
 547            ]
 548        );
 549    }
 550
 551    #[test]
 552    fn file_header_extra_space() {
 553        let options = ["--- file", "---   file", "---\tfile"];
 554
 555        for option in options {
 556            pretty_assertions::assert_eq!(
 557                DiffLine::parse(option),
 558                DiffLine::OldPath {
 559                    path: "file".into()
 560                },
 561                "{option}",
 562            );
 563        }
 564    }
 565
 566    #[test]
 567    fn hunk_header_extra_space() {
 568        let options = [
 569            "@@ -1,2 +1,3 @@",
 570            "@@  -1,2  +1,3 @@",
 571            "@@\t-1,2\t+1,3\t@@",
 572            "@@ -1,2  +1,3 @@",
 573            "@@ -1,2   +1,3 @@",
 574            "@@ -1,2 +1,3   @@",
 575            "@@ -1,2 +1,3 @@ garbage",
 576        ];
 577
 578        for option in options {
 579            pretty_assertions::assert_eq!(
 580                DiffLine::parse(option),
 581                DiffLine::HunkHeader(Some(HunkLocation {
 582                    start_line_old: 0,
 583                    count_old: 2,
 584                    start_line_new: 0,
 585                    count_new: 3
 586                })),
 587                "{option}",
 588            );
 589        }
 590    }
 591
 592    #[test]
 593    fn hunk_header_without_location() {
 594        pretty_assertions::assert_eq!(DiffLine::parse("@@ ... @@"), DiffLine::HunkHeader(None));
 595    }
 596
 597    #[test]
 598    fn test_parse_path() {
 599        assert_eq!(parse_header_path("a/", "foo.txt"), "foo.txt");
 600        assert_eq!(
 601            parse_header_path("a/", "foo/bar/baz.txt"),
 602            "foo/bar/baz.txt"
 603        );
 604        assert_eq!(parse_header_path("a/", "a/foo.txt"), "foo.txt");
 605        assert_eq!(
 606            parse_header_path("a/", "a/foo/bar/baz.txt"),
 607            "foo/bar/baz.txt"
 608        );
 609
 610        // Extra
 611        assert_eq!(
 612            parse_header_path("a/", "a/foo/bar/baz.txt  2025"),
 613            "foo/bar/baz.txt"
 614        );
 615        assert_eq!(
 616            parse_header_path("a/", "a/foo/bar/baz.txt\t2025"),
 617            "foo/bar/baz.txt"
 618        );
 619        assert_eq!(
 620            parse_header_path("a/", "a/foo/bar/baz.txt \""),
 621            "foo/bar/baz.txt"
 622        );
 623
 624        // Quoted
 625        assert_eq!(
 626            parse_header_path("a/", "a/foo/bar/\"baz quox.txt\""),
 627            "foo/bar/baz quox.txt"
 628        );
 629        assert_eq!(
 630            parse_header_path("a/", "\"a/foo/bar/baz quox.txt\""),
 631            "foo/bar/baz quox.txt"
 632        );
 633        assert_eq!(
 634            parse_header_path("a/", "\"foo/bar/baz quox.txt\""),
 635            "foo/bar/baz quox.txt"
 636        );
 637        assert_eq!(parse_header_path("a/", "\"whatever 🤷\""), "whatever 🤷");
 638        assert_eq!(
 639            parse_header_path("a/", "\"foo/bar/baz quox.txt\"  2025"),
 640            "foo/bar/baz quox.txt"
 641        );
 642        // unescaped quotes are dropped
 643        assert_eq!(parse_header_path("a/", "foo/\"bar\""), "foo/bar");
 644
 645        // Escaped
 646        assert_eq!(
 647            parse_header_path("a/", "\"foo/\\\"bar\\\"/baz.txt\""),
 648            "foo/\"bar\"/baz.txt"
 649        );
 650        assert_eq!(
 651            parse_header_path("a/", "\"C:\\\\Projects\\\\My App\\\\old file.txt\""),
 652            "C:\\Projects\\My App\\old file.txt"
 653        );
 654    }
 655
 656    #[test]
 657    fn test_parse_diff_with_leading_and_trailing_garbage() {
 658        let diff = indoc! {"
 659            I need to make some changes.
 660
 661            I'll change the following things:
 662            - one
 663              - two
 664            - three
 665
 666            ```
 667            --- a/file.txt
 668            +++ b/file.txt
 669             one
 670            +AND
 671             two
 672            ```
 673
 674            Summary of what I did:
 675            - one
 676              - two
 677            - three
 678
 679            That's about it.
 680        "};
 681
 682        let mut events = Vec::new();
 683        let mut parser = DiffParser::new(diff);
 684        while let Some(event) = parser.next().unwrap() {
 685            events.push(event);
 686        }
 687
 688        assert_eq!(
 689            events,
 690            &[
 691                DiffEvent::Hunk {
 692                    path: "file.txt".into(),
 693                    hunk: Hunk {
 694                        context: "one\ntwo\n".into(),
 695                        edits: vec![Edit {
 696                            range: 4..4,
 697                            text: "AND\n".into()
 698                        }],
 699                    }
 700                },
 701                DiffEvent::FileEnd { renamed_to: None }
 702            ],
 703        )
 704    }
 705
 706    #[gpui::test]
 707    async fn test_apply_diff_successful(cx: &mut TestAppContext) {
 708        let fs = init_test(cx);
 709
 710        let buffer_1_text = indoc! {r#"
 711            one
 712            two
 713            three
 714            four
 715            five
 716        "# };
 717
 718        let buffer_1_text_final = indoc! {r#"
 719            3
 720            4
 721            5
 722        "# };
 723
 724        let buffer_2_text = indoc! {r#"
 725            six
 726            seven
 727            eight
 728            nine
 729            ten
 730        "# };
 731
 732        let buffer_2_text_final = indoc! {r#"
 733            5
 734            six
 735            seven
 736            7.5
 737            eight
 738            nine
 739            ten
 740            11
 741        "# };
 742
 743        fs.insert_tree(
 744            path!("/root"),
 745            json!({
 746                "file1": buffer_1_text,
 747                "file2": buffer_2_text,
 748            }),
 749        )
 750        .await;
 751
 752        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 753
 754        let diff = indoc! {r#"
 755            --- a/root/file1
 756            +++ b/root/file1
 757             one
 758             two
 759            -three
 760            +3
 761             four
 762             five
 763            --- a/root/file1
 764            +++ b/root/file1
 765             3
 766            -four
 767            -five
 768            +4
 769            +5
 770            --- a/root/file1
 771            +++ b/root/file1
 772            -one
 773            -two
 774             3
 775             4
 776            --- a/root/file2
 777            +++ b/root/file2
 778            +5
 779             six
 780            --- a/root/file2
 781            +++ b/root/file2
 782             seven
 783            +7.5
 784             eight
 785            --- a/root/file2
 786            +++ b/root/file2
 787             ten
 788            +11
 789        "#};
 790
 791        let _buffers = apply_diff(diff, &project, &mut cx.to_async())
 792            .await
 793            .unwrap();
 794        let buffer_1 = project
 795            .update(cx, |project, cx| {
 796                let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
 797                project.open_buffer(project_path, cx)
 798            })
 799            .await
 800            .unwrap();
 801
 802        buffer_1.read_with(cx, |buffer, _cx| {
 803            assert_eq!(buffer.text(), buffer_1_text_final);
 804        });
 805        let buffer_2 = project
 806            .update(cx, |project, cx| {
 807                let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
 808                project.open_buffer(project_path, cx)
 809            })
 810            .await
 811            .unwrap();
 812
 813        buffer_2.read_with(cx, |buffer, _cx| {
 814            assert_eq!(buffer.text(), buffer_2_text_final);
 815        });
 816    }
 817
 818    #[gpui::test]
 819    async fn test_apply_diff_non_unique(cx: &mut TestAppContext) {
 820        let fs = init_test(cx);
 821
 822        let buffer_1_text = indoc! {r#"
 823            one
 824            two
 825            three
 826            four
 827            five
 828            one
 829            two
 830            three
 831            four
 832            five
 833        "# };
 834
 835        fs.insert_tree(
 836            path!("/root"),
 837            json!({
 838                "file1": buffer_1_text,
 839            }),
 840        )
 841        .await;
 842
 843        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 844        let buffer = project
 845            .update(cx, |project, cx| {
 846                project.open_local_buffer(path!("/root/file1"), cx)
 847            })
 848            .await
 849            .unwrap();
 850        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 851
 852        let diff = indoc! {r#"
 853            --- a/root/file1
 854            +++ b/root/file1
 855             one
 856             two
 857            -three
 858            +3
 859             four
 860             five
 861        "#};
 862
 863        let final_text = indoc! {r#"
 864            one
 865            two
 866            three
 867            four
 868            five
 869            one
 870            two
 871            3
 872            four
 873            five
 874        "#};
 875
 876        apply_diff(diff, &project, &mut cx.to_async())
 877            .await
 878            .expect_err("Non-unique edits should fail");
 879
 880        let ranges = [buffer_snapshot.anchor_before(Point::new(1, 0))
 881            ..buffer_snapshot.anchor_after(buffer_snapshot.max_point())];
 882
 883        let (edited_snapshot, edits) = parse_diff(diff, |_path| Some((&buffer_snapshot, &ranges)))
 884            .await
 885            .unwrap();
 886
 887        assert_eq!(edited_snapshot.remote_id(), buffer_snapshot.remote_id());
 888        buffer.update(cx, |buffer, cx| {
 889            buffer.edit(edits, None, cx);
 890            assert_eq!(buffer.text(), final_text);
 891        });
 892    }
 893
 894    #[gpui::test]
 895    async fn test_parse_diff_with_edits_within_line(cx: &mut TestAppContext) {
 896        let fs = init_test(cx);
 897
 898        let buffer_1_text = indoc! {r#"
 899            one two three four
 900            five six seven eight
 901            nine ten eleven twelve
 902        "# };
 903
 904        fs.insert_tree(
 905            path!("/root"),
 906            json!({
 907                "file1": buffer_1_text,
 908            }),
 909        )
 910        .await;
 911
 912        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 913        let buffer = project
 914            .update(cx, |project, cx| {
 915                project.open_local_buffer(path!("/root/file1"), cx)
 916            })
 917            .await
 918            .unwrap();
 919        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 920
 921        let diff = indoc! {r#"
 922            --- a/root/file1
 923            +++ b/root/file1
 924             one two three four
 925            -five six seven eight
 926            +five SIX seven eight!
 927             nine ten eleven twelve
 928        "#};
 929
 930        let (buffer, edits) = parse_diff(diff, |_path| {
 931            Some((&buffer_snapshot, &[(Anchor::MIN..Anchor::MAX)] as &[_]))
 932        })
 933        .await
 934        .unwrap();
 935
 936        let edits = edits
 937            .into_iter()
 938            .map(|(range, text)| (range.to_point(&buffer), text))
 939            .collect::<Vec<_>>();
 940        assert_eq!(
 941            edits,
 942            &[
 943                (Point::new(1, 5)..Point::new(1, 8), "SIX".into()),
 944                (Point::new(1, 20)..Point::new(1, 20), "!".into())
 945            ]
 946        );
 947    }
 948
 949    #[gpui::test]
 950    async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
 951        let fs = init_test(cx);
 952
 953        let start = indoc! {r#"
 954            one
 955            two
 956            three
 957            four
 958            five
 959
 960            four
 961            five
 962        "# };
 963
 964        let end = indoc! {r#"
 965            one
 966            two
 967            3
 968            four
 969            5
 970
 971            four
 972            five
 973        "# };
 974
 975        fs.insert_tree(
 976            path!("/root"),
 977            json!({
 978                "file1": start,
 979            }),
 980        )
 981        .await;
 982
 983        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 984
 985        let diff = indoc! {r#"
 986            --- a/root/file1
 987            +++ b/root/file1
 988             one
 989             two
 990            -three
 991            +3
 992             four
 993            -five
 994            +5
 995        "#};
 996
 997        let _buffers = apply_diff(diff, &project, &mut cx.to_async())
 998            .await
 999            .unwrap();
1000
1001        let buffer_1 = project
1002            .update(cx, |project, cx| {
1003                let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
1004                project.open_buffer(project_path, cx)
1005            })
1006            .await
1007            .unwrap();
1008
1009        buffer_1.read_with(cx, |buffer, _cx| {
1010            assert_eq!(buffer.text(), end);
1011        });
1012    }
1013
1014    fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
1015        cx.update(|cx| {
1016            let settings_store = SettingsStore::test(cx);
1017            cx.set_global(settings_store);
1018        });
1019
1020        FakeFs::new(cx.background_executor.clone())
1021    }
1022}