diff --git a/yazi-core/src/completion/completion.rs b/yazi-core/src/completion/completion.rs index ee67e1c1..3429b2e8 100644 --- a/yazi-core/src/completion/completion.rs +++ b/yazi-core/src/completion/completion.rs @@ -5,20 +5,24 @@ pub struct Completion { items: Vec, cursor: usize, - pub position: Position, - pub visible: bool, + pub position: Position, + pub column_cnt: u8, + pub max_width: u16, + pub visible: bool, } impl Completion { pub fn show(&mut self, opt: CompletionOpt) { - self.close(false); + self.close(); self.visible = true; self.items = opt.items; self.position = opt.position; + self.column_cnt = opt.column_cnt; + self.max_width = opt.max_width; } - pub fn close(&mut self, submit: bool) -> bool { + pub fn close(&mut self) -> bool { self.cursor = 0; self.visible = false; true @@ -33,8 +37,6 @@ impl Completion { let old = self.cursor; self.cursor = (self.cursor + step).min(len - 1); - eprintln!("Cur: {}", self.items.get(self.cursor).unwrap()); - old != self.cursor } @@ -47,5 +49,7 @@ impl Completion { pub fn list(&self) -> Vec { self.items.clone() } - pub fn selected_cursor(&self) -> usize { self.cursor } + pub fn cursor(&self) -> usize { self.cursor } + + pub fn get_selection(&self) -> Option { self.items.get(self.cursor).cloned() } } diff --git a/yazi-core/src/completion/option.rs b/yazi-core/src/completion/option.rs index 03b59f74..f6c3e1c0 100644 --- a/yazi-core/src/completion/option.rs +++ b/yazi-core/src/completion/option.rs @@ -3,28 +3,34 @@ use ratatui::layout::Rect; use crate::Position; pub struct CompletionOpt { - pub items: Vec, - pub position: Position, + pub items: Vec, + pub position: Position, + pub column_cnt: u8, + pub max_width: u16, } impl CompletionOpt { pub fn top() -> Self { Self { - items: vec![], - position: Position::Top( + items: vec![], + position: Position::Top( // TODO: hardcode - Rect { x: 0, y: 5, width: 100, height: 10 }, + Rect { x: 0, y: 5, width: 80, height: 10 }, ), + column_cnt: 4, + max_width: 20, } } pub fn hover() -> Self { Self { - items: vec![], - position: Position::Hovered( + items: vec![], + position: Position::Hovered( // TODO: hardcode - Rect { x: 0, y: 3, width: 40, height: 4 }, + Rect { x: 0, y: 3, width: 80, height: 10 }, ), + column_cnt: 4, + max_width: 20, } } diff --git a/yazi-core/src/input/completion.rs b/yazi-core/src/input/completion.rs index 4d1900fa..b61c5dd3 100644 --- a/yazi-core/src/input/completion.rs +++ b/yazi-core/src/input/completion.rs @@ -1,23 +1,47 @@ +use crossterm::event::KeyCode; +use yazi_config::keymap::Key; + use crate::{completion::CompletionOpt, input::Input}; impl Input { pub fn complete(&mut self) -> bool { if !self.completion.visible { - eprintln!("Completing 1: {}", self.completion_callback.is_some()); - let current = self.snaps.current().value.clone(); - let result = self.completion_callback.as_ref().map(|f| f(current)).unwrap_or_default(); - eprintln!("Completing {:?}", result); + let current = self.snaps.current().value.as_str(); + let result = self.init_completion.as_ref().map(|f| f(current)).unwrap_or_default(); match result.len() { 0 => false, - 1 => self.type_str(result.get(0).unwrap()), + 1 => { + if let Some(f) = self.finish_completion.as_ref() { + self.replace_str(f(current, result.get(0).unwrap()).as_str()) + } else { + false + } + } _ => { self.completion.show(CompletionOpt::top().with_items(result)); true } } } else { - eprintln!("Completing @"); - self.completion.next(1) + false } } + + pub fn navigate_completion(&mut self, key: &Key) -> bool { + match key.code { + KeyCode::Up => self.completion.prev(self.completion.column_cnt as usize), + KeyCode::Down => self.completion.next(self.completion.column_cnt as usize), + KeyCode::Left => self.completion.prev(1), + KeyCode::Right | KeyCode::Tab => self.completion.next(1), + _ => false, + } + } + + pub fn finish_completion(&mut self) -> bool { + if let (Some(val), Some(f)) = (self.completion.get_selection(), &self.finish_completion) { + let final_val = f(self.snaps.current().value.as_str(), val.as_str()); + self.replace_str(final_val.as_str()); + } + self.completion.close() + } } diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 479c2d0c..66e0a946 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -24,8 +24,9 @@ pub struct Input { // Shell pub(super) highlight: bool, - pub completion: Completion, - pub(super) completion_callback: Option Vec + Send>>, + pub completion: Completion, + pub(super) init_completion: Option Vec + Send>>, + pub(super) finish_completion: Option String + Send>>, } impl Input { @@ -44,7 +45,8 @@ impl Input { // Shell self.highlight = opt.highlight; - self.completion_callback = opt.completion_callback + self.init_completion = opt.init_completion; + self.finish_completion = opt.finish_completion; } pub fn close(&mut self, submit: bool) -> bool { @@ -54,7 +56,7 @@ impl Input { } self.visible = false; - self.completion.close(false); + self.completion.close(); true } @@ -195,10 +197,28 @@ impl Input { return false; } - eprintln!("Type key {:?}", key); - match key { - Key { code: KeyCode::Tab, shift: false, ctrl: false, alt: false } => return self.complete(), - _ => (), + if self.completion.visible { + match key { + Key { + code: KeyCode::Tab | KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right, + shift: false, + ctrl: false, + alt: false, + } => return self.navigate_completion(key), + Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false } => { + return self.finish_completion(); + } + _ => { + self.completion.close(); + } + } + } else { + match key { + Key { code: KeyCode::Tab, shift: false, ctrl: false, alt: false } => { + return self.complete(); + } + _ => (), + } } if let Some(c) = key.plain() { @@ -229,6 +249,13 @@ impl Input { self.move_(s.chars().count() as isize) } + pub fn replace_str(&mut self, s: &str) -> bool { + let snap = self.snaps.current_mut(); + snap.value = s.to_string(); + self.flush_value(); + self.move_(s.chars().count() as isize) + } + pub fn backspace(&mut self) -> bool { let snap = self.snaps.current_mut(); if snap.cursor < 1 { diff --git a/yazi-core/src/input/option.rs b/yazi-core/src/input/option.rs index 04fa3eb6..e9c1aece 100644 --- a/yazi-core/src/input/option.rs +++ b/yazi-core/src/input/option.rs @@ -3,40 +3,43 @@ use ratatui::prelude::Rect; use crate::Position; pub struct InputOpt { - pub title: String, - pub value: String, - pub position: Position, - pub realtime: bool, - pub highlight: bool, - pub completion_callback: Option Vec + Send>>, + pub title: String, + pub value: String, + pub position: Position, + pub realtime: bool, + pub highlight: bool, + pub init_completion: Option Vec + Send>>, + pub finish_completion: Option String + Send>>, } impl InputOpt { pub fn top(title: impl AsRef) -> Self { Self { - title: title.as_ref().to_owned(), - value: String::new(), - position: Position::Top( + title: title.as_ref().to_owned(), + value: String::new(), + position: Position::Top( // TODO: hardcode Rect { x: 0, y: 2, width: 50, height: 3 }, ), - realtime: false, - highlight: false, - completion_callback: None, + realtime: false, + highlight: false, + init_completion: None, + finish_completion: None, } } pub fn hovered(title: impl AsRef) -> Self { Self { - title: title.as_ref().to_owned(), - value: String::new(), - position: Position::Hovered( + title: title.as_ref().to_owned(), + value: String::new(), + position: Position::Hovered( // TODO: hardcode Rect { x: 0, y: 1, width: 50, height: 3 }, ), - realtime: false, - highlight: false, - completion_callback: None, + realtime: false, + highlight: false, + init_completion: None, + finish_completion: None, } } @@ -59,11 +62,16 @@ impl InputOpt { } #[inline] - pub fn with_completion Vec + Send + 'static>( + pub fn with_completion< + F1: Fn(&str) -> Vec + Send + 'static, + F2: Fn(&str, &str) -> String + Send + 'static, + >( mut self, - callback: F, + init_completion: F1, + finish_completion: F2, ) -> Self { - self.completion_callback = Some(Box::new(callback)); + self.init_completion = Some(Box::new(init_completion)); + self.finish_completion = Some(Box::new(finish_completion)); self } } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index 9b3dc016..ade0361c 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -60,9 +60,44 @@ impl Tab { pub fn cd_interactive(&mut self, target: Url) -> bool { tokio::spawn(async move { let mut result = emit!(Input( - InputOpt::top("Change directory:") - .with_value(target.to_string_lossy()) - .with_completion(|s| vec![s, "Hi".to_string()]) + InputOpt::top("Change directory:").with_value(target.to_string_lossy()).with_completion( + |prefix| { + let mut cmp_prefix = prefix; + if prefix.contains('/') { + let (old_prefix, old_file_prefix) = prefix.rsplit_once('/').unwrap(); + cmp_prefix = old_file_prefix; + std::fs::read_dir(old_prefix.to_string() + "/") + } else { + std::fs::read_dir(".") + } + .ok() + .map(|list| { + list + .filter_map(|file| { + file + .ok() + .map(|f| { + let name = f.file_name().to_string_lossy().to_string(); + if f.metadata().is_ok_and(|m| m.is_dir()) && name.starts_with(&cmp_prefix) { + Some(name) + } else { + None + } + }) + .flatten() + }) + .collect() + }) + .unwrap_or_default() + }, + |current, new| { + if let Some((prefix, _)) = current.rsplit_once("/") { + format!("{prefix}/{new}/") + } else { + format!("{new}/") + } + } + ) )); if let Some(Ok(s)) = result.recv().await { diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index 7cd4d154..9e048853 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -17,26 +17,26 @@ impl<'a> Widget for Completion<'a> { let completion = &self.cx.input.completion; let area = self.cx.area(&completion.position); - const COLUMN_CNT: usize = 4; - const MAX_WIDTH: usize = 20; - let constraint = - (0..COLUMN_CNT).map(|_| Constraint::Ratio(1, MAX_WIDTH as u32)).collect::>(); + let constraint = (0..completion.column_cnt) + .map(|_| Constraint::Percentage(completion.max_width)) + .collect::>(); let table = { + let max_width = completion.max_width as usize; let mut table = vec![]; let mut cur_row = vec![]; for (idx, s) in completion.list().into_iter().enumerate() { - if idx != 0 && idx % COLUMN_CNT == 0 { + if idx != 0 && idx % completion.column_cnt as usize == 0 { let t = mem::take(&mut cur_row); table.push(Row::new(t)); } - // todo cur_row.push( - Cell::from(if s.len() < MAX_WIDTH { + Cell::from(if s.len() < max_width { s } else { - s.split_at(MAX_WIDTH - 1).0.to_string() + "…" + s.split_at(max_width - 1).0.to_string() + "…" }) - .style(if completion.selected_cursor() == idx { + // todo + .style(if completion.cursor() == idx { THEME.select.active.into() } else { THEME.select.inactive.into()