//! 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)
}