WIP contents diff

[?]
Apr 28, 2025, 5:35 PM
MJDGPSHGF62FTVWZBE7MFNJTUQD42OBVJEOSVPBT553UFJLTEMXQC

Dependencies

  • [2] 6YZAVBWU Initial commit
  • [3] EC3TVL4X add untracked files
  • [4] KT5UYXGK fix selection after adding file, add changed file diffs
  • [5] A5YBC77V record!
  • [6] D7A7MSIH allow to defer or abandon record, add buttons
  • [7] 4WO3ZJM2 show untracked files' contents
  • [8] CFYW3HGZ wip: display changed files
  • [9] AMPZ2BXK show changed files diffs (only Edit atm)
  • [10] AXSXZQDG fix updating changed file contents, styling
  • [11] V55EAIWQ add src file LRU cache
  • [12] NRCUG4R2 load changed files src when selected
  • [13] Y5ATDI2H convert changed file diffs and load src only if any needs it
  • [14] YBLPPHZN show contents for move, del and undel
  • [15] UMO6U2ZT partition the change files diffs on whether they have content
  • [16] HOJZI52Y rename flowers_ui to inflorescence
  • [17] W4LFX7IH group diffs by file name
  • [18] WT3GA27P add cursor with selection
  • [*] SWWE2R6M display basic repo stuff

Change contents

  • edit in crates/libflowers_client/src/repo.rs at line 7
    [3.29]
    [5.114]
    use std::cmp;
  • replacement in crates/libflowers_client/src/repo.rs at line 49
    [4.263][4.263:335]()
    #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, strum::Display)]
    [4.263]
    [4.335]
    #[derive(Clone, Debug, PartialEq, Eq, strum::Display)]
  • edit in crates/libflowers_client/src/repo.rs at line 75
    [4.575]
    [9.198]
    }
    impl PartialOrd for ChangedFileDiff {
    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
    diff_line(self)
    .zip(diff_line(other))
    .map(|(left, right)| left.cmp(&right))
    }
  • edit in crates/libflowers_client/src/repo.rs at line 85
    [9.201]
    [9.201]
    impl Ord for ChangedFileDiff {
    fn cmp(&self, other: &Self) -> cmp::Ordering {
    diff_line(self)
    .zip(diff_line(other))
    .map(|(left, right)| left.cmp(&right))
    // TODO better ordering by type
    .unwrap_or(cmp::Ordering::Equal)
    }
    }
    fn diff_line(diff: &ChangedFileDiff) -> Option<usize> {
    match diff {
    ChangedFileDiff::Move => None,
    ChangedFileDiff::Del => None,
    ChangedFileDiff::Undel => None,
    ChangedFileDiff::Add => None,
    ChangedFileDiff::SolveNameConflict => None,
    ChangedFileDiff::UnsolveNameConflict => None,
    ChangedFileDiff::Edit { line, .. } => Some(*line),
    ChangedFileDiff::Replacement { line, .. } => Some(*line),
    ChangedFileDiff::SolveOrderConflict => None,
    ChangedFileDiff::UnsolveOrderConflict => None,
    ChangedFileDiff::ResurrectZombines => None,
    ChangedFileDiff::AddRoot => None,
    ChangedFileDiff::DelRoot => None,
    }
    }
  • edit in crates/inflorescence/src/main.rs at line 1
    [2.2763]
    [9.1207]
    mod contents_diff;
  • edit in crates/inflorescence/src/main.rs at line 194
    [14.42][14.42:52]()
    Move,
  • edit in crates/inflorescence/src/main.rs at line 198
    [12.174]
    [11.2231]
    Move,
  • replacement in crates/inflorescence/src/main.rs at line 888
    [15.699][15.699:768]()
    acc_with.push(ChangedFileDiffWithContents::Move)
    [15.699]
    [15.768]
    acc_without.push(ChangedFileDiffWithoutContents::Move)
  • replacement in crates/inflorescence/src/main.rs at line 1151
    [13.6545][13.6545:6593](),[9.5261][8.364:392](),[13.6593][8.364:392](),[8.364][8.364:392](),[8.392][10.381:444](),[10.444][9.5262:5330](),[8.447][9.5262:5330](),[9.5330][8.495:515](),[8.495][8.495:515]()
    let diffs = [el(text("TODO"))];
    el(column([
    view_diff_header(format!("{path} diff:")),
    el(scrollable(column(diffs).spacing(SPACING))),
    ]))
    [13.6545]
    [7.9691]
    match state.src_files_cache.peek(path) {
    Some(SrcFile::Loaded(content)) => {
    let file_content = match content {
    FileEditorContent::Decoded(content) => {
    content.text()
    }
    FileEditorContent::ShortBase64(_) => todo!(),
    FileEditorContent::UnknownEncoding => todo!(),
    };
    let with_contents = diffs
    .with_contents
    .iter()
    .map(|change| match dbg!(change) {
    ChangedFileDiffWithContents::Add => {
    contents_diff::ChangedFileDiffWithContents::Add
    }
    ChangedFileDiffWithContents::Edit {
    line,
    deleted,
    contents,
    } => contents_diff::ChangedFileDiffWithContents::Edit {
    line: *line,
    deleted: *deleted,
    contents: match contents {
    FileEditorContent::Decoded(content) => contents_diff::ChangeContents::Decoded(content.text()) ,
    FileEditorContent::ShortBase64(_) => todo!(),
    FileEditorContent::UnknownEncoding => todo!(),
    },
    },
    ChangedFileDiffWithContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => contents_diff::ChangedFileDiffWithContents::Replacement {
    line:*line ,
    change_contents: match change_contents {
    FileEditorContent::Decoded(content) => contents_diff::ChangeContents::Decoded(content.text()) ,
    FileEditorContent::ShortBase64(_) => todo!(),
    FileEditorContent::UnknownEncoding => todo!(),
    },
    replacement_contents: match replacement_contents {
    FileEditorContent::Decoded(content) => contents_diff::ChangeContents::Decoded(content.text()) ,
    FileEditorContent::ShortBase64(_) => todo!(),
    FileEditorContent::UnknownEncoding => todo!(),
    }
    },
    ChangedFileDiffWithContents::Del => contents_diff::ChangedFileDiffWithContents::Del,
    ChangedFileDiffWithContents::Undel => contents_diff::ChangedFileDiffWithContents::Undel,
    })
    .collect::<Vec<_>>();
    let state =
    contents_diff::init(&file_content, &with_contents);
    let diffs =
    contents_diff::view(state).map(|msg| todo!());
    el(column([
    view_diff_header(format!("{path} diff:")),
    el(scrollable(diffs)),
    ]))
    }
    Some(SrcFile::Loading) => el(text("loading...")),
    None => todo!(),
    }
  • replacement in crates/inflorescence/src/main.rs at line 1265
    [6.4039][6.4039:4079]()
    fn el<'a, E, M>(e: E) -> Element<'a, M>
    [6.4039]
    [6.4079]
    pub fn el<'a, E, M>(e: E) -> Element<'a, M>
  • file addition: contents_diff.rs (----------)
    [16.120]
    //! A changed file's contents diff.
    use std::cmp;
    use iced::widget::{column, row, text};
    use iced::{Element, Font};
    use crate::el;
    #[derive(Debug)]
    pub enum ChangedFileDiffWithContents {
    Add,
    Edit {
    line: usize,
    deleted: bool,
    contents: ChangeContents,
    },
    Replacement {
    line: usize,
    /// Deleted line
    change_contents: ChangeContents,
    /// Added lines
    replacement_contents: ChangeContents,
    },
    Del,
    Undel,
    }
    #[derive(Debug)]
    pub enum ChangeContents {
    Decoded(String),
    /// Short byte sequence of unknown encoding encoded with base64 for
    /// display. Must be shorter than [`MAX_LEN_BASE64_DISPLAY`]
    ShortBase64(String),
    UnknownEncoding,
    }
    #[derive(Debug)]
    pub struct State {
    pub sections: Vec<Section>,
    pub max_line_num: usize,
    }
    #[derive(Debug)]
    pub enum Section {
    Unchanged(Lines),
    /// `deleted` and `added` are together because for
    /// `ChangedFileDiffWithContents::Replacement` they begin on the same line
    /// number
    Changed {
    deleted: Lines,
    added: Lines,
    },
    }
    pub fn init(
    file_contents: &str,
    changes: &[ChangedFileDiffWithContents],
    ) -> State {
    let changes_len = changes.len();
    let mut file_lines = file_contents.split('\n');
    let mut sections = Vec::with_capacity(changes.len());
    let mut current_line: usize = 1;
    let mut max_line_num: usize = 1;
    for change in changes {
    match change {
    ChangedFileDiffWithContents::Add
    | ChangedFileDiffWithContents::Undel => {
    debug_assert_eq!(changes_len, 1);
    let added: Vec<_> = file_lines.map(str::to_string).collect();
    let max_line_num = added.len();
    sections.push(Section::Changed {
    deleted: vec![],
    added,
    });
    return State {
    sections,
    max_line_num,
    };
    }
    ChangedFileDiffWithContents::Del => {
    debug_assert_eq!(changes_len, 1);
    let deleted: Vec<_> = file_lines.map(str::to_string).collect();
    let max_line_num = deleted.len();
    sections.push(Section::Changed {
    deleted,
    added: vec![],
    });
    return State {
    sections,
    max_line_num,
    };
    }
    ChangedFileDiffWithContents::Edit {
    line,
    deleted,
    contents,
    } => {
    let lines_before = line - current_line;
    if lines_before != 0 {
    current_line = *line;
    sections.push(Section::Unchanged(take_n_lines(
    &mut file_lines,
    lines_before,
    )));
    }
    if *deleted {
    let deleted = contents_to_lines(contents);
    max_line_num = current_line + deleted.len();
    sections.push(Section::Changed {
    deleted,
    added: vec![],
    });
    } else {
    let added = contents_to_lines(contents);
    current_line += added.len();
    max_line_num = current_line;
    drop_n_lines(&mut file_lines, added.len());
    sections.push(Section::Changed {
    deleted: vec![],
    added,
    });
    }
    }
    ChangedFileDiffWithContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => {
    let lines_before = line - current_line;
    if lines_before != 0 {
    current_line = *line;
    sections.push(Section::Unchanged(take_n_lines(
    &mut file_lines,
    lines_before,
    )));
    }
    let added = contents_to_lines(replacement_contents);
    let deleted = contents_to_lines(change_contents);
    max_line_num =
    current_line + cmp::max(added.len(), deleted.len());
    current_line += added.len();
    drop_n_lines(&mut file_lines, added.len());
    sections.push(Section::Changed { deleted, added });
    }
    }
    }
    let rest: Lines = file_lines.map(str::to_string).collect();
    if !rest.is_empty() {
    max_line_num = current_line + rest.len();
    sections.push(Section::Unchanged(rest));
    }
    State {
    sections,
    max_line_num,
    }
    }
    fn contents_to_lines(contents: &ChangeContents) -> Lines {
    match contents {
    ChangeContents::Decoded(string) => {
    string.split('\n').map(str::to_string).collect()
    }
    ChangeContents::ShortBase64(_) => todo!(),
    ChangeContents::UnknownEncoding => todo!(),
    }
    }
    fn take_n_lines(file_lines: &mut std::str::Split<'_, char>, n: usize) -> Lines {
    let mut lines = Vec::with_capacity(n);
    for _ in 0..n {
    lines.push(file_lines.next().unwrap().to_string());
    }
    lines
    }
    fn drop_n_lines(file_lines: &mut std::str::Split<'_, char>, n: usize) {
    for _ in 0..n {
    file_lines.next().unwrap();
    }
    }
    /// INVARIANT: There must be no new-lines in any of the strings, the source
    /// string must be split on those.
    pub type Lines = Vec<String>;
    #[derive(Debug)]
    pub enum Msg {}
    pub fn update(state: &mut State, msg: Msg) {}
    pub fn view(state: State) -> Element<'static, Msg> {
    let mut current_line = 1;
    let line_num_digits = state.max_line_num.to_string().len();
    let sections_view = state
    .sections
    .iter()
    .flat_map(|section| match section {
    Section::Unchanged(lines) => {
    let res = lines
    .iter()
    .enumerate()
    .map(|(ix, line)| {
    el(row([
    el(code(" ")),
    el(code(format!(
    "{:width$} ",
    current_line + ix,
    width = line_num_digits
    ))),
    el(code(line.clone())),
    ]))
    })
    .collect::<Vec<_>>();
    current_line += lines.len();
    res
    }
    Section::Changed { deleted, added } => {
    let res = deleted
    .iter()
    .enumerate()
    .map(|(ix, line)| {
    el(row([
    el(code("- ")),
    el(code(format!(
    "{:width$} ",
    current_line + ix,
    width = line_num_digits
    ))),
    el(code(line.clone())),
    ]))
    })
    .chain(added.iter().enumerate().map(|(ix, line)| {
    el(row([
    el(code("+ ")),
    el(code(format!(
    "{:width$} ",
    current_line + ix,
    width = line_num_digits
    ))),
    el(code(line.clone())),
    ]))
    }))
    .collect::<Vec<_>>();
    current_line += added.len();
    res
    }
    })
    .collect::<Vec<_>>();
    el(column(sections_view))
    }
    fn code<'a>(txt: impl text::IntoFragment<'a>) -> iced::widget::Text<'a> {
    text(txt).font(Font::MONOSPACE)
    }