//! Selection of changes to include or exclude in the next record.
use libflorescence::prelude::*;
use libflorescence::repo;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Msg {
ToggleOverall,
ToggleFile {
file: String,
},
ToggleChange {
file: String,
change: repo::ChangedFileDiff,
},
}
#[derive(Clone, Copy, Debug)]
pub enum Pick {
Include,
Exclude,
}
#[derive(Clone, Copy, Debug, Default)]
pub enum PickSet {
#[default]
Include,
Exclude,
Partial,
}
#[derive(Clone, Debug, Default)]
pub struct State {
/// State of all files
pub overall: PickSet,
/// Map from file name to its changes state. This is preserved even when
/// `overall` selection changes.
pub files: HashMap<String, PickSet>,
/// Map from file name to its individual change state. This is preserved
/// even when `overall` and `files` selection changes
pub changes: HashMap<String, PartialFile>,
}
#[derive(Clone, Debug, Default)]
pub struct PartialFile {
/// Map from file name to its state
// NOTE: When it's possible to select individual lines/chars to record,
// then the value would be `PickSet` and the inner-most value of
// selected ranges would be `Pick`
pub changes: HashMap<repo::ChangedFileDiff, Pick>,
}
pub fn determine_file(
state: &State,
file: &str,
changed_files: &repo::ChangedFiles,
) -> PickSet {
match state.overall {
PickSet::Include => PickSet::Include,
PickSet::Exclude => PickSet::Exclude,
PickSet::Partial => {
if let Some(pick) = state.files.get(file) {
*pick
} else if file_has_any_partial_change(file, state, changed_files) {
PickSet::Partial
} else {
default_pick_set(state)
}
}
}
}
pub fn update(state: &mut State, msg: Msg, changed_files: &repo::ChangedFiles) {
match msg {
Msg::ToggleOverall => {
state.overall = next_overall_pick(state, changed_files);
}
Msg::ToggleFile { file } => {
if matches!(state.overall, PickSet::Include | PickSet::Exclude) {
// Clear out the last partial states
for changed_file in changed_files.keys() {
state.files.insert(changed_file.clone(), state.overall);
}
state.changes.clear();
}
let pick = next_file_pick(&file, state, changed_files);
state.files.insert(file, pick);
state.overall = determine_overall_pick(state, changed_files);
}
Msg::ToggleChange { file, change } => {
if matches!(state.overall, PickSet::Include | PickSet::Exclude) {
// Clear out the last partial states
for changed_file in changed_files.keys() {
state.files.insert(changed_file.clone(), state.overall);
}
state.changes.clear();
}
let current = state
.changes
.get(&file)
.and_then(|file| file.changes.get(&change).copied())
.unwrap_or_else(|| default_pick(state));
let pick = next_pick(current);
let partial_file =
state.changes.entry(file.clone()).or_insert_with(default);
partial_file.changes.insert(change, pick);
let file_pick = determine_file_pick(&file, state, changed_files)
.unwrap_or(match pick {
Pick::Include => PickSet::Include,
Pick::Exclude => PickSet::Exclude,
});
state.files.insert(file, file_pick);
state.overall = determine_overall_pick(state, changed_files);
}
}
dbg!(&state);
}
pub fn next_overall_pick(
state: &State,
changed_files: &repo::ChangedFiles,
) -> PickSet {
let maybe_next = next_pick_set(state.overall);
if let PickSet::Partial = maybe_next
&& !any_partial_change(state, changed_files)
{
next_pick_set(maybe_next)
} else {
maybe_next
}
}
pub fn next_file_pick(
file: &str,
state: &State,
changed_files: &repo::ChangedFiles,
) -> PickSet {
let current = state
.files
.get(file)
.copied()
.unwrap_or_else(|| default_pick_set(state));
let maybe_next = next_pick_set(current);
if let PickSet::Partial = maybe_next
&& !file_has_any_partial_change(file, state, changed_files)
{
next_pick_set(maybe_next)
} else {
maybe_next
}
}
fn next_pick_set(pick: PickSet) -> PickSet {
match pick {
PickSet::Include => PickSet::Partial,
PickSet::Partial => PickSet::Exclude,
PickSet::Exclude => PickSet::Include,
}
}
fn next_pick(pick: Pick) -> Pick {
match pick {
Pick::Include => Pick::Exclude,
Pick::Exclude => Pick::Include,
}
}
/// Determine the default value for a file that is not explicitly set.
fn default_pick_set(state: &State) -> PickSet {
match default_pick(state) {
Pick::Include => PickSet::Include,
Pick::Exclude => PickSet::Exclude,
}
}
/// Determine the default value for a change that is not explicitly set.
fn default_pick(state: &State) -> Pick {
let State {
overall,
files,
changes,
} = state;
if matches!(overall, PickSet::Partial)
|| files
.values()
.any(|pick| matches!(pick, PickSet::Partial | PickSet::Exclude))
|| changes.values().any(|file| {
file.changes
.values()
.any(|pick| matches!(pick, Pick::Exclude))
})
{
Pick::Exclude
} else {
Pick::Include
}
}
fn any_partial_change(
state: &State,
changed_files: &repo::ChangedFiles,
) -> bool {
let files = changed_files.keys();
if state.files.len() == files.len() {
let (mut has_include, mut has_exclude) = (false, false);
state.files.iter().any(|(file, pick)| {
has_exclude |= matches!(pick, PickSet::Exclude);
has_include |= matches!(pick, PickSet::Include);
(has_include && has_exclude)
|| matches!(pick, PickSet::Partial)
|| file_has_any_partial_change(file, state, changed_files)
})
} else {
state.files.iter().any(|(file, pick)| {
matches!(pick, PickSet::Exclude | PickSet::Partial)
|| file_has_any_partial_change(file, state, changed_files)
})
}
}
fn file_has_any_partial_change(
file: &str,
state: &State,
changed_files: &repo::ChangedFiles,
) -> bool {
let changes = changed_files.get(file).unwrap();
let (mut has_include, mut has_exclude) = (false, false);
state
.changes
.get(file)
.map(|file| {
if file.changes.len() == changes.len() {
// The number of of changes in selection is the same as number
// of actual changes, we need to have both exclude and include
// to be partial
file.changes.values().any(|pick| {
has_exclude |= matches!(pick, Pick::Exclude);
has_include |= matches!(pick, Pick::Include);
has_include && has_exclude
})
} else {
// There is the same number of changes in selection is not the
// same as number of actual changes, we only need to find 1
// exclusion to be partial
file.changes
.values()
.any(|pick| matches!(pick, Pick::Exclude))
}
})
.unwrap_or_default()
}
fn determine_overall_pick(
state: &State,
changed_files: &repo::ChangedFiles,
) -> PickSet {
let files = changed_files.keys();
let mut pick = None;
if state.files.len() != files.len() {
pick = Some(default_pick_set(state));
}
for file_pick in state.files.values() {
if let Some(pick) = &mut pick {
*pick = match (*pick, *file_pick) {
(PickSet::Include, PickSet::Include) => PickSet::Include,
(PickSet::Exclude, PickSet::Exclude) => PickSet::Exclude,
(PickSet::Include, PickSet::Exclude)
| (PickSet::Include, PickSet::Partial)
| (PickSet::Exclude, PickSet::Include)
| (PickSet::Exclude, PickSet::Partial)
| (PickSet::Partial, PickSet::Include)
| (PickSet::Partial, PickSet::Exclude)
| (PickSet::Partial, PickSet::Partial) => PickSet::Partial,
};
if let PickSet::Partial = pick {
break;
}
} else {
pick = Some(*file_pick);
}
}
pick.unwrap_or(PickSet::Include)
}
fn determine_file_pick(
file: &str,
state: &State,
changed_files: &repo::ChangedFiles,
) -> Option<PickSet> {
let changes = changed_files.get(file).unwrap();
let mut pick = None;
if let Some(file) = state.changes.get(file) {
if file.changes.len() != changes.len() {
pick = Some(default_pick_set(state));
}
for change_pick in file.changes.values() {
if let Some(pick) = &mut pick {
*pick = match (*pick, change_pick) {
(PickSet::Include, Pick::Include) => PickSet::Include,
(PickSet::Exclude, Pick::Exclude) => PickSet::Exclude,
(PickSet::Exclude, Pick::Include)
| (PickSet::Include, Pick::Exclude)
| (PickSet::Partial, Pick::Include)
| (PickSet::Partial, Pick::Exclude) => PickSet::Partial,
};
if let PickSet::Partial = pick {
break;
}
} else {
pick = Some(match change_pick {
Pick::Include => PickSet::Include,
Pick::Exclude => PickSet::Include,
});
}
}
}
pick
}