#[doc(inline)]
pub use inflorescence_model::diff::{
contents_to_lines, id_parts_hash, init_diffs_nav, Combined, DecodedFile,
DiffWithContents, DiffWithoutContents, File, FileAndState, FileContent,
FilesState, IdHash, Lines, Section, State, UndecodableContents,
UndecodableFile,
};
use inflorescence_iced_widget::nav_scrollable;
use libflorescence::repo;
use tracing::error;
use std::cmp;
use std::collections::HashSet;
pub fn init_file(
file_content: FileContent<'_>,
changed_file: Option<&repo::ChangedFile>,
) -> File {
let (diffs_with_contents, diffs_without_contents) = changed_file
.map(from_repo_changed_file)
.unwrap_or((vec![], vec![]));
match file_content {
FileContent::Decoded(file_content) => {
let combined =
combine_decoded_contents(&file_content, diffs_with_contents);
File::Decoded(DecodedFile {
combined,
diffs_without_contents,
})
}
FileContent::UnknownEncoding => File::Undecodable(UndecodableFile {
diffs_with_contents,
diffs_without_contents,
}),
FileContent::Image(bytes) => {
File::Image(bytes)
}
}
}
pub fn from_repo_changed_file(
changed_file: &repo::ChangedFile,
) -> (Vec<(DiffWithContents, IdHash)>, Vec<DiffWithoutContents>) {
changed_file.iter().fold((vec![], vec![]), |(mut with, mut without), diff| {
match diff {
repo::ChangedFileDiff::Move{ old_path } => {
without.push(DiffWithoutContents::Move { old_path: old_path.clone() });
},
repo::ChangedFileDiff::MoveEdge => {
without.push(DiffWithoutContents::MoveEdge);
},
repo::ChangedFileDiff::Del { contents } => {
let contents = match contents {
Some(repo::Contents::Decoded(lines) | repo::Contents::ShortBase64(lines)) => Some(lines.clone()),
Some(repo::Contents::UnknownEncoding(_)) | None => None
};
without.push(DiffWithoutContents::Del);
with.push((DiffWithContents::Del{ contents }, id_parts_hash(diff)));
},
repo::ChangedFileDiff::Undel => {
with.push((DiffWithContents::Undel, id_parts_hash(diff)));
},
repo::ChangedFileDiff::Add { contents } => {
let contents = match contents {
Some(repo::Contents::Decoded(lines) | repo::Contents::ShortBase64(lines)) => Some(lines.clone()),
Some(repo::Contents::UnknownEncoding(_)) | None => None
};
with.push((DiffWithContents::Add { contents }, id_parts_hash(diff)));
},
repo::ChangedFileDiff::SolveNameConflict => {
without.push(DiffWithoutContents::SolveNameConflict);
},
repo::ChangedFileDiff::UnsolveNameConflict => {
without.push(DiffWithoutContents::UnsolveNameConflict);
},
repo::ChangedFileDiff::Edit { line, deleted, contents } => match contents{
repo::Contents::Decoded(lines) => {
with.push((DiffWithContents::Edit {
line: *line,
deleted: *deleted,
contents: lines.clone(),
}, id_parts_hash(diff)));
},
repo::Contents::ShortBase64(short) => {
without.push(DiffWithoutContents::Edit {
line: *line,
deleted: *deleted,
contents: UndecodableContents::ShortBase64(short.clone()),
});
},
repo::Contents::UnknownEncoding(_bytes) => {
without.push(DiffWithoutContents::Edit {
line: *line,
deleted: *deleted,
contents: UndecodableContents::UnknownEncoding,
});
},
},
repo::ChangedFileDiff::Replacement { line, change_contents, replacement_contents } => match (change_contents, replacement_contents) {
(repo::Contents::Decoded(change), repo::Contents::Decoded(replacement)) => {
with.push((DiffWithContents::Replacement {
line: *line,
change_contents: change.clone(),
replacement_contents: replacement.clone(),
}, id_parts_hash(diff)));
},
(repo::Contents::ShortBase64(change), repo::Contents::ShortBase64(replacement)) => {
without.push(DiffWithoutContents::Replacement {
line: *line,
change_contents: UndecodableContents::ShortBase64(change.clone()),
replacement_contents: UndecodableContents::ShortBase64(replacement.clone()),
});
},
(_, repo::Contents::UnknownEncoding(_)) |
(repo::Contents::UnknownEncoding(_), _) => {
without.push(DiffWithoutContents::Replacement {
line: *line,
change_contents: UndecodableContents::UnknownEncoding,
replacement_contents: UndecodableContents::UnknownEncoding,
});
},
_ => {
error!("The change and replacement have different encoding! Change: {change_contents:?}, replacement: {replacement_contents:?}");
without.push(DiffWithoutContents::Replacement {
line: *line,
change_contents: UndecodableContents::UnknownEncoding,
replacement_contents: UndecodableContents::UnknownEncoding,
});
}
},
repo::ChangedFileDiff::SolveOrderConflict => {
without.push(DiffWithoutContents::SolveOrderConflict);
},
repo::ChangedFileDiff::UnsolveOrderConflict => {
without.push(DiffWithoutContents::UnsolveOrderConflict);
},
repo::ChangedFileDiff::ResurrectZombines => {
without.push(DiffWithoutContents::ResurrectZombines);
},
repo::ChangedFileDiff::AddRoot => {
without.push(DiffWithoutContents::AddRoot);
},
repo::ChangedFileDiff::DelRoot => {
without.push(DiffWithoutContents::DelRoot);
},
};
(with, without)
})
}
pub fn should_file_exist(changed_file: &repo::ChangedFile) -> bool {
for change in changed_file {
match change {
repo::ChangedFileDiff::Del { .. }
| repo::ChangedFileDiff::AddRoot
| repo::ChangedFileDiff::DelRoot => return false,
repo::ChangedFileDiff::Move { .. }
| repo::ChangedFileDiff::MoveEdge
| repo::ChangedFileDiff::Undel
| repo::ChangedFileDiff::Add { .. }
| repo::ChangedFileDiff::SolveNameConflict
| repo::ChangedFileDiff::UnsolveNameConflict
| repo::ChangedFileDiff::Edit { .. }
| repo::ChangedFileDiff::Replacement { .. }
| repo::ChangedFileDiff::SolveOrderConflict
| repo::ChangedFileDiff::UnsolveOrderConflict
| repo::ChangedFileDiff::ResurrectZombines => {}
}
}
true
}
pub fn unchanged_sections(diff: &File) -> HashSet<usize> {
match diff {
File::Decoded(file) => file
.combined
.sections
.iter()
.enumerate()
.filter_map(|(ix, section)| match section {
Section::Unchanged(_) => Some(ix),
Section::Changed { .. } => None,
})
.collect(),
File::Undecodable(_) | File::Image(_) => HashSet::new(),
}
}
pub fn file_diff_needs_scrolling(files_diffs: &FilesState) -> bool {
nav_scrollable::needs_scrolling(&files_diffs.diffs_nav)
}
fn combine_decoded_contents(
file_content: &str,
diffs_with_contents: Vec<(DiffWithContents, IdHash)>,
) -> Combined {
let changes_len = diffs_with_contents.len();
let mut file_lines = trim_line_break_suffix(file_content).split('\n');
let mut sections = Vec::with_capacity(diffs_with_contents.len());
let mut current_line: usize = 1;
let mut max_line_num: usize = 1;
for (change, diff_id) in diffs_with_contents {
match change {
DiffWithContents::Add { contents } => {
let added = contents
.as_deref()
.map(trim_line_break_suffix)
.map(contents_to_lines)
.unwrap_or_default();
let max_line_num = added.len();
sections.push(Section::Changed {
deleted: vec![],
added,
diff_id,
});
return Combined {
sections,
max_line_num,
};
}
DiffWithContents::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,
diff_id,
});
return Combined {
sections,
max_line_num,
};
}
DiffWithContents::Del { contents } => {
let deleted = contents
.as_deref()
.map(trim_line_break_suffix)
.map(contents_to_lines)
.unwrap_or_default();
let max_line_num = deleted.len();
sections.push(Section::Changed {
deleted,
added: vec![],
diff_id,
});
return Combined {
sections,
max_line_num,
};
}
DiffWithContents::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(trim_line_break_suffix(&contents));
max_line_num = current_line + deleted.len();
sections.push(Section::Changed {
deleted,
added: vec![],
diff_id,
});
} else {
let added =
contents_to_lines(trim_line_break_suffix(&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,
diff_id,
});
}
}
DiffWithContents::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(trim_line_break_suffix(
&replacement_contents,
));
let deleted =
contents_to_lines(trim_line_break_suffix(&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,
diff_id,
});
}
}
}
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));
}
Combined {
sections,
max_line_num,
}
}
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();
}
}
fn trim_line_break_suffix(text: &str) -> &str {
trim_suffix(text, '\n')
}
fn trim_suffix(text: &str, suffix: char) -> &str {
text.strip_suffix(suffix).unwrap_or(text)
}