use crate::FileList;
pub fn move_cursor(cursor: usize, delta: isize, len: usize) -> usize {
if len == 0 {
return 0;
}
if delta < 0 {
cursor.saturating_sub((-delta) as usize)
} else {
(cursor + delta as usize).min(len - 1)
}
}
pub fn page_up_cursor(cursor: usize, viewport_height: usize) -> usize {
let jump = viewport_height.saturating_sub(1).max(1);
cursor.saturating_sub(jump)
}
pub fn page_down_cursor(cursor: usize, viewport_height: usize, len: usize) -> usize {
if len == 0 {
return 0;
}
let jump = viewport_height.saturating_sub(1).max(1);
(cursor + jump).min(len.saturating_sub(1))
}
pub fn update_scroll_offset(
cursor: usize,
scroll_offset: usize,
padding: usize,
viewport_height: usize,
len: usize,
) -> usize {
if len == 0 || viewport_height == 0 {
return 0;
}
let max_scroll = len.saturating_sub(viewport_height);
if cursor < scroll_offset + padding {
return cursor.saturating_sub(padding).min(max_scroll);
}
let visible_end = scroll_offset + viewport_height;
if cursor >= visible_end.saturating_sub(padding) {
let new_scroll =
cursor.saturating_sub(viewport_height.saturating_sub(1).saturating_sub(padding));
return new_scroll.min(max_scroll);
}
scroll_offset.min(max_scroll)
}
pub fn find_char_match(files: &FileList, start_cursor: usize, c: char) -> Option<usize> {
let len = files.len();
if len == 0 {
return None;
}
let c_lower = c.to_lowercase().next().unwrap_or(c);
let start = (start_cursor + 1) % len;
for i in 0..len {
let idx = (start + i) % len;
if let Some(first) = files[idx].name.chars().next() {
if first.to_lowercase().next() == Some(c_lower) {
return Some(idx);
}
}
}
None
}
pub fn find_substring_match(files: &FileList, start_cursor: usize, query: &str) -> Option<usize> {
if query.is_empty() {
return None;
}
let len = files.len();
if len == 0 {
return None;
}
let query_lower = query.to_lowercase();
let start = (start_cursor + 1) % len;
for i in 0..len {
let idx = (start + i) % len;
if files[idx].name.to_lowercase().contains(&query_lower) {
return Some(idx);
}
}
None
}
#[allow(dead_code)] pub fn find_by_name(files: &FileList, name: &str) -> Option<usize> {
files.iter().position(|e| e.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_move_cursor_down() {
assert_eq!(move_cursor(0, 1, 10), 1);
assert_eq!(move_cursor(5, 1, 10), 6);
assert_eq!(move_cursor(9, 1, 10), 9); }
#[test]
fn test_move_cursor_up() {
assert_eq!(move_cursor(5, -1, 10), 4);
assert_eq!(move_cursor(0, -1, 10), 0); }
#[test]
fn test_move_cursor_empty() {
assert_eq!(move_cursor(0, 1, 0), 0);
assert_eq!(move_cursor(0, -1, 0), 0);
}
#[test]
fn test_page_up_cursor() {
assert_eq!(page_up_cursor(20, 10), 11); assert_eq!(page_up_cursor(5, 10), 0); assert_eq!(page_up_cursor(0, 10), 0);
}
#[test]
fn test_page_down_cursor() {
assert_eq!(page_down_cursor(0, 10, 50), 9); assert_eq!(page_down_cursor(45, 10, 50), 49); assert_eq!(page_down_cursor(0, 10, 0), 0); }
#[test]
fn test_update_scroll_offset_top_boundary() {
assert_eq!(update_scroll_offset(10, 5, 2, 10, 50), 5);
assert_eq!(update_scroll_offset(3, 5, 2, 10, 50), 1);
assert_eq!(update_scroll_offset(7, 5, 2, 10, 50), 5); }
#[test]
fn test_update_scroll_offset_bottom_boundary() {
let result = update_scroll_offset(49, 0, 2, 10, 50);
assert!(
result > 0,
"Should scroll when cursor at end: got {}",
result
);
assert!(result + 10 > 49, "Cursor should be visible");
let result = update_scroll_offset(8, 0, 2, 10, 50);
assert!(result >= 1, "Should scroll when cursor near bottom edge");
assert_eq!(update_scroll_offset(7, 0, 2, 10, 50), 0);
}
#[test]
fn test_update_scroll_offset_max_scroll() {
let result = update_scroll_offset(14, 0, 2, 10, 15);
assert!(result <= 5, "Scroll should not exceed max: got {}", result);
}
#[test]
fn test_update_scroll_offset_empty() {
assert_eq!(update_scroll_offset(0, 0, 2, 10, 0), 0);
assert_eq!(update_scroll_offset(0, 0, 2, 0, 10), 0);
}
}