mod input;
mod render;
use crate::model::ConflictFile;
use crate::ui::navigation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolveAction {
None,
Back,
ResolveExternal(String),
ResolveOurs(String),
ResolveTheirs(String),
ShowDiff(String),
}
#[derive(Debug, Clone)]
pub struct ResolveView {
pub revision: String,
pub is_working_copy: bool,
files: Vec<ConflictFile>,
selected_index: usize,
scroll_offset: usize,
}
impl ResolveView {
pub fn new(revision: String, is_working_copy: bool, files: Vec<ConflictFile>) -> Self {
Self {
revision,
is_working_copy,
files,
selected_index: 0,
scroll_offset: 0,
}
}
#[allow(dead_code)] pub fn files(&self) -> &[ConflictFile] {
&self.files
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn selected_file_path(&self) -> Option<&str> {
self.files.get(self.selected_index).map(|f| f.path.as_str())
}
pub fn set_files(&mut self, files: Vec<ConflictFile>) {
self.files = files;
if !self.files.is_empty() {
self.selected_index = self.selected_index.min(self.files.len() - 1);
} else {
self.selected_index = 0;
}
}
pub fn move_down(&mut self) {
let max = self.files.len().saturating_sub(1);
self.selected_index = navigation::select_next(self.selected_index, max);
}
pub fn move_up(&mut self) {
self.selected_index = navigation::select_prev(self.selected_index);
}
pub fn move_to_top(&mut self) {
self.selected_index = 0;
}
pub fn move_to_bottom(&mut self) {
if !self.files.is_empty() {
self.selected_index = self.files.len() - 1;
}
}
fn calculate_scroll_offset(&self, visible_height: usize) -> usize {
navigation::adjust_scroll(self.selected_index, self.scroll_offset, visible_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_files() -> Vec<ConflictFile> {
vec![
ConflictFile {
path: "test.txt".to_string(),
description: "2-sided conflict".to_string(),
},
ConflictFile {
path: "src/main.rs".to_string(),
description: "2-sided conflict".to_string(),
},
ConflictFile {
path: "src/lib.rs".to_string(),
description: "3-sided conflict".to_string(),
},
]
}
#[test]
fn test_resolve_view_new() {
let view = ResolveView::new("abc12345".to_string(), true, make_test_files());
assert_eq!(view.revision, "abc12345");
assert!(view.is_working_copy);
assert_eq!(view.file_count(), 3);
assert!(!view.is_empty());
}
#[test]
fn test_resolve_view_empty() {
let view = ResolveView::new("abc12345".to_string(), false, vec![]);
assert!(view.is_empty());
assert_eq!(view.file_count(), 0);
assert_eq!(view.selected_file_path(), None);
}
#[test]
fn test_resolve_view_navigation() {
let mut view = ResolveView::new("abc12345".to_string(), true, make_test_files());
assert_eq!(view.selected_file_path(), Some("test.txt"));
view.move_down();
assert_eq!(view.selected_file_path(), Some("src/main.rs"));
view.move_down();
assert_eq!(view.selected_file_path(), Some("src/lib.rs"));
view.move_down();
assert_eq!(view.selected_file_path(), Some("src/lib.rs"));
view.move_up();
assert_eq!(view.selected_file_path(), Some("src/main.rs"));
view.move_to_top();
assert_eq!(view.selected_file_path(), Some("test.txt"));
view.move_to_bottom();
assert_eq!(view.selected_file_path(), Some("src/lib.rs"));
}
#[test]
fn test_resolve_view_set_files() {
let mut view = ResolveView::new("abc12345".to_string(), true, make_test_files());
view.move_to_bottom();
view.set_files(vec![ConflictFile {
path: "remaining.txt".to_string(),
description: "2-sided conflict".to_string(),
}]);
assert_eq!(view.selected_index, 0);
assert_eq!(view.selected_file_path(), Some("remaining.txt"));
}
#[test]
fn test_resolve_view_set_files_empty() {
let mut view = ResolveView::new("abc12345".to_string(), true, make_test_files());
view.set_files(vec![]);
assert!(view.is_empty());
assert_eq!(view.selected_index, 0);
}
#[test]
fn test_resolve_view_not_working_copy() {
let view = ResolveView::new("lqwwsqpm".to_string(), false, make_test_files());
assert!(!view.is_working_copy);
assert_eq!(view.revision, "lqwwsqpm");
}
}