From 5b66f6fcf5b3509dde4d95a2171c26724b8d146f Mon Sep 17 00:00:00 2001 From: XOR-op <17672363+XOR-op@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:30:52 -0400 Subject: [PATCH] feat: auto-completion for input component (#324) --- cspell.json | 2 +- yazi-config/preset/keymap.toml | 15 + yazi-config/preset/theme.toml | 15 + yazi-config/src/keymap/exec.rs | 10 +- yazi-config/src/keymap/key.rs | 2 +- yazi-config/src/keymap/keymap.rs | 36 ++- yazi-config/src/theme/theme.rs | 26 +- yazi-core/src/completion/commands/arrow.rs | 44 +++ yazi-core/src/completion/commands/close.rs | 25 ++ yazi-core/src/completion/commands/mod.rs | 4 + yazi-core/src/completion/commands/show.rs | 62 ++++ yazi-core/src/completion/commands/trigger.rs | 110 +++++++ yazi-core/src/completion/completion.rs | 31 ++ yazi-core/src/completion/mod.rs | 4 + yazi-core/src/context.rs | 70 ++--- yazi-core/src/input/commands/complete.rs | 45 +++ yazi-core/src/input/commands/mod.rs | 1 + yazi-core/src/input/input.rs | 66 +++-- yazi-core/src/input/mod.rs | 2 +- yazi-core/src/input/option.rs | 34 ++- yazi-core/src/lib.rs | 1 + yazi-core/src/position.rs | 18 +- yazi-core/src/tab/commands/cd.rs | 30 +- yazi-fm/src/app.rs | 6 +- yazi-fm/src/completion/completion.rs | 62 ++++ yazi-fm/src/completion/mod.rs | 3 + yazi-fm/src/executor.rs | 284 +++++++++++-------- yazi-fm/src/input/input.rs | 2 +- yazi-fm/src/main.rs | 1 + yazi-fm/src/root.rs | 6 +- yazi-plugin/src/bindings/mod.rs | 1 + yazi-shared/src/errors/input.rs | 2 + yazi-shared/src/term/mod.rs | 3 +- 33 files changed, 776 insertions(+), 247 deletions(-) create mode 100644 yazi-core/src/completion/commands/arrow.rs create mode 100644 yazi-core/src/completion/commands/close.rs create mode 100644 yazi-core/src/completion/commands/mod.rs create mode 100644 yazi-core/src/completion/commands/show.rs create mode 100644 yazi-core/src/completion/commands/trigger.rs create mode 100644 yazi-core/src/completion/completion.rs create mode 100644 yazi-core/src/completion/mod.rs create mode 100644 yazi-core/src/input/commands/complete.rs create mode 100644 yazi-core/src/input/commands/mod.rs create mode 100644 yazi-fm/src/completion/completion.rs create mode 100644 yazi-fm/src/completion/mod.rs diff --git a/cspell.json b/cspell.json index bd61a192..b2751baa 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi"],"language":"en","version":"0.2","flagWords":[]} +{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit"],"flagWords":[],"version":"0.2","language":"en"} diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 57d62670..7bb81fed 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -237,3 +237,18 @@ keymap = [ # Filtering { on = [ "/" ], exec = "filter", desc = "Apply a filter for the help items" }, ] + +[completion] + +keymap = [ + { on = [ "" ], exec = "close", desc = "Cancel completion" }, + { on = [ "" ], exec = "close --submit", desc = "Submit the completion" }, + + { on = [ "" ], exec = "arrow -1", desc = "Move cursor up" }, + { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, + + { on = [ "" ], exec = "arrow -1", desc = "Move cursor up" }, + { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, + + { on = [ "~" ], exec = "help", desc = "Open help" } +] diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index 57a558cc..4a8f023e 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -85,6 +85,21 @@ inactive = {} # : }}} +# : Completion {{{ + +[completion] +border = { fg = "blue" } +active = { bg = "darkgray" } +inactive = {} + +# Icons +icon_file = "" +icon_folder = "" +icon_command = "" + +# : }}} + + # : Tasks {{{ [tasks] diff --git a/yazi-config/src/keymap/exec.rs b/yazi-config/src/keymap/exec.rs index 207088a5..9548470a 100644 --- a/yazi-config/src/keymap/exec.rs +++ b/yazi-config/src/keymap/exec.rs @@ -102,9 +102,15 @@ impl Exec { pub fn vec(self) -> Vec { vec![self] } #[inline] - pub fn with_bool(mut self, name: &str, state: bool) -> Self { + pub fn with(mut self, name: impl ToString, value: impl ToString) -> Self { + self.named.insert(name.to_string(), value.to_string()); + self + } + + #[inline] + pub fn with_bool(mut self, name: impl ToString, state: bool) -> Self { if state { - self.named.insert(name.to_string(), "".to_string()); + self.named.insert(name.to_string(), Default::default()); } self } diff --git a/yazi-config/src/keymap/key.rs b/yazi-config/src/keymap/key.rs index 7e59b33c..a5f56e96 100644 --- a/yazi-config/src/keymap/key.rs +++ b/yazi-config/src/keymap/key.rs @@ -2,7 +2,7 @@ use anyhow::bail; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::Deserialize; -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] #[serde(try_from = "String")] pub struct Key { pub code: KeyCode, diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index d4ef0795..77be1670 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -7,11 +7,12 @@ use crate::MERGED_KEYMAP; #[derive(Debug)] pub struct Keymap { - pub manager: Vec, - pub tasks: Vec, - pub select: Vec, - pub input: Vec, - pub help: Vec, + pub manager: Vec, + pub tasks: Vec, + pub select: Vec, + pub input: Vec, + pub help: Vec, + pub completion: Vec, } impl<'de> Deserialize<'de> for Keymap { @@ -21,11 +22,12 @@ impl<'de> Deserialize<'de> for Keymap { { #[derive(Deserialize)] struct Shadow { - manager: Inner, - tasks: Inner, - select: Inner, - input: Inner, - help: Inner, + manager: Inner, + tasks: Inner, + select: Inner, + input: Inner, + help: Inner, + completion: Inner, } #[derive(Deserialize)] struct Inner { @@ -34,11 +36,12 @@ impl<'de> Deserialize<'de> for Keymap { let shadow = Shadow::deserialize(deserializer)?; Ok(Self { - manager: shadow.manager.keymap, - tasks: shadow.tasks.keymap, - select: shadow.select.keymap, - input: shadow.input.keymap, - help: shadow.help.keymap, + manager: shadow.manager.keymap, + tasks: shadow.tasks.keymap, + select: shadow.select.keymap, + input: shadow.input.keymap, + help: shadow.help.keymap, + completion: shadow.completion.keymap, }) } } @@ -56,6 +59,7 @@ impl Keymap { KeymapLayer::Select => &self.select, KeymapLayer::Input => &self.input, KeymapLayer::Help => &self.help, + KeymapLayer::Completion => &self.completion, KeymapLayer::Which => unreachable!(), } } @@ -69,6 +73,7 @@ pub enum KeymapLayer { Select, Input, Help, + Completion, Which, } @@ -80,6 +85,7 @@ impl Display for KeymapLayer { KeymapLayer::Select => write!(f, "select"), KeymapLayer::Input => write!(f, "input"), KeymapLayer::Help => write!(f, "help"), + KeymapLayer::Completion => write!(f, "completion"), KeymapLayer::Which => write!(f, "which"), } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index dc1558b5..80d4f02c 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -81,6 +81,17 @@ pub struct Select { pub inactive: Style, } +#[derive(Deserialize, Serialize)] +pub struct Completion { + pub border: Style, + pub active: Style, + pub inactive: Style, + + pub icon_file: String, + pub icon_folder: String, + pub icon_command: String, +} + #[derive(Deserialize, Serialize)] pub struct Tasks { pub border: Style, @@ -111,13 +122,14 @@ pub struct Help { #[derive(Deserialize, Serialize)] pub struct Theme { - pub manager: Manager, - status: Status, - pub input: Input, - pub select: Select, - pub tasks: Tasks, - pub which: Which, - pub help: Help, + pub manager: Manager, + status: Status, + pub input: Input, + pub select: Select, + pub completion: Completion, + pub tasks: Tasks, + pub which: Which, + pub help: Help, // File-specific styles #[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)] diff --git a/yazi-core/src/completion/commands/arrow.rs b/yazi-core/src/completion/commands/arrow.rs new file mode 100644 index 00000000..735b5e94 --- /dev/null +++ b/yazi-core/src/completion/commands/arrow.rs @@ -0,0 +1,44 @@ +use yazi_config::keymap::Exec; + +use crate::completion::Completion; + +pub struct Opt(isize); + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self(e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0)) } +} + +impl Completion { + fn next(&mut self, step: usize) -> bool { + let len = self.cands.len(); + if len == 0 { + return false; + } + + let old = self.cursor; + self.cursor = (self.cursor + step).min(len - 1); + + let limit = self.limit(); + if self.cursor >= len.min(self.offset + limit) { + self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old); + } + + old != self.cursor + } + + fn prev(&mut self, step: usize) -> bool { + let old = self.cursor; + self.cursor = self.cursor.saturating_sub(step); + + if self.cursor < self.offset { + self.offset = self.offset.saturating_sub(old - self.cursor); + } + + old != self.cursor + } + + pub fn arrow(&mut self, opt: impl Into) -> bool { + let step = opt.into().0; + if step > 0 { self.next(step as usize) } else { self.prev(step.unsigned_abs()) } + } +} diff --git a/yazi-core/src/completion/commands/close.rs b/yazi-core/src/completion/commands/close.rs new file mode 100644 index 00000000..ea3eaa65 --- /dev/null +++ b/yazi-core/src/completion/commands/close.rs @@ -0,0 +1,25 @@ +use yazi_config::keymap::{Exec, KeymapLayer}; + +use crate::{completion::Completion, emit}; + +pub struct Opt(bool); + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self(e.named.contains_key("submit")) } +} + +impl Completion { + pub fn close(&mut self, opt: impl Into) -> bool { + let submit = opt.into().0; + if submit { + emit!(Call( + Exec::call("complete", vec![self.selected().into()]).with("ticket", self.ticket).vec(), + KeymapLayer::Input + )); + } + + self.caches.clear(); + self.visible = false; + true + } +} diff --git a/yazi-core/src/completion/commands/mod.rs b/yazi-core/src/completion/commands/mod.rs new file mode 100644 index 00000000..7e958bd6 --- /dev/null +++ b/yazi-core/src/completion/commands/mod.rs @@ -0,0 +1,4 @@ +mod arrow; +mod close; +mod show; +mod trigger; diff --git a/yazi-core/src/completion/commands/show.rs b/yazi-core/src/completion/commands/show.rs new file mode 100644 index 00000000..7040200a --- /dev/null +++ b/yazi-core/src/completion/commands/show.rs @@ -0,0 +1,62 @@ +use std::ops::ControlFlow; + +use yazi_config::keymap::Exec; + +use crate::completion::Completion; + +pub struct Opt<'a> { + cache: &'a Vec, + cache_name: &'a str, + word: &'a str, + ticket: usize, +} + +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { + Self { + cache: &e.args, + cache_name: e.named.get("cache-name").map(|n| n.as_str()).unwrap_or_default(), + word: e.named.get("word").map(|w| w.as_str()).unwrap_or_default(), + ticket: e.named.get("ticket").and_then(|v| v.parse().ok()).unwrap_or(0), + } + } +} + +impl Completion { + pub fn show<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into(); + if self.ticket != opt.ticket { + return false; + } + + if !opt.cache.is_empty() { + self.caches.insert(opt.cache_name.to_owned(), opt.cache.clone()); + } + let Some(cache) = self.caches.get(opt.cache_name) else { + return false; + }; + + let flow = cache.iter().try_fold(Vec::with_capacity(30), |mut v, s| { + if s.contains(opt.word) && s != opt.word { + v.push(s.to_owned()); + if v.len() >= 30 { + return ControlFlow::Break(v); + } + } + ControlFlow::Continue(v) + }); + + self.cands = match flow { + ControlFlow::Continue(v) => v, + ControlFlow::Break(v) => v, + }; + self.ticket = opt.ticket; + + if !self.cands.is_empty() { + self.offset = 0; + self.cursor = 0; + self.visible = true; + } + true + } +} diff --git a/yazi-core/src/completion/commands/trigger.rs b/yazi-core/src/completion/commands/trigger.rs new file mode 100644 index 00000000..5ed07e07 --- /dev/null +++ b/yazi-core/src/completion/commands/trigger.rs @@ -0,0 +1,110 @@ +use std::mem; + +use tokio::fs; +use yazi_config::keymap::{Exec, KeymapLayer}; + +use crate::{completion::Completion, emit}; + +pub struct Opt<'a> { + before: &'a str, + ticket: usize, +} + +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { + Self { + before: e.named.get("before").map(|s| s.as_str()).unwrap_or_default(), + ticket: e.named.get("ticket").and_then(|s| s.parse().ok()).unwrap_or(0), + } + } +} + +impl Completion { + #[inline] + fn split_path(s: &str) -> (String, String) { + match s.rsplit_once(|c| c == '/' || c == '\\') { + Some((p, c)) => (format!("{p}/"), c.to_owned()), + None => (".".to_owned(), s.to_owned()), + } + } + + pub fn trigger<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into(); + if opt.ticket < self.ticket { + return false; + } + + self.ticket = opt.ticket; + let (parent, child) = Self::split_path(opt.before); + + if self.caches.contains_key(&parent) { + return self.show( + &Exec::call("show", vec![]) + .with("cache-name", parent) + .with("word", child) + .with("ticket", opt.ticket), + ); + } + + let ticket = self.ticket; + tokio::spawn(async move { + let mut dir = fs::read_dir(&parent).await?; + let mut cache = Vec::new(); + while let Ok(Some(f)) = dir.next_entry().await { + let Ok(meta) = f.metadata().await else { + continue; + }; + + let sep = if !meta.is_dir() { + "" + } else if cfg!(windows) { + "\\" + } else { + "/" + }; + cache.push(format!("{}{sep}", f.file_name().to_string_lossy())); + } + + if !cache.is_empty() { + emit!(Call( + Exec::call("show", cache) + .with("cache-name", parent) + .with("word", child) + .with("ticket", ticket) + .vec(), + KeymapLayer::Completion + )); + } + + Ok::<(), anyhow::Error>(()) + }); + + mem::replace(&mut self.visible, false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_explode() { + assert_eq!(Completion::split_path(""), (".".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path(" "), (".".to_owned(), " ".to_owned())); + assert_eq!(Completion::split_path("/"), ("/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("//"), ("//".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("/foo"), ("/".to_owned(), "foo".to_owned())); + assert_eq!(Completion::split_path("/foo/"), ("/foo/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("/foo/bar"), ("/foo/".to_owned(), "bar".to_owned())); + + // Windows + assert_eq!(Completion::split_path("foo"), (".".to_owned(), "foo".to_owned())); + assert_eq!(Completion::split_path("foo\\"), ("foo/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("foo\\bar"), ("foo/".to_owned(), "bar".to_owned())); + assert_eq!(Completion::split_path("foo\\bar\\"), ("foo\\bar/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("C:\\"), ("C:/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("C:\\foo"), ("C:/".to_owned(), "foo".to_owned())); + assert_eq!(Completion::split_path("C:\\foo\\"), ("C:\\foo/".to_owned(), "".to_owned())); + assert_eq!(Completion::split_path("C:\\foo\\bar"), ("C:\\foo/".to_owned(), "bar".to_owned())); + } +} diff --git a/yazi-core/src/completion/completion.rs b/yazi-core/src/completion/completion.rs new file mode 100644 index 00000000..a1c72ceb --- /dev/null +++ b/yazi-core/src/completion/completion.rs @@ -0,0 +1,31 @@ +use std::collections::BTreeMap; + +#[derive(Default)] +pub struct Completion { + pub(super) caches: BTreeMap>, + pub(super) cands: Vec, + pub(super) offset: usize, + pub cursor: usize, + + pub(super) ticket: usize, + pub visible: bool, +} + +impl Completion { + // --- Cands + #[inline] + pub fn window(&self) -> &[String] { + let end = (self.offset + self.limit()).min(self.cands.len()); + &self.cands[self.offset..end] + } + + #[inline] + pub fn limit(&self) -> usize { self.cands.len().min(10) } + + #[inline] + pub fn selected(&self) -> &String { &self.cands[self.cursor] } + + // --- Cursor + #[inline] + pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } +} diff --git a/yazi-core/src/completion/mod.rs b/yazi-core/src/completion/mod.rs new file mode 100644 index 00000000..daf831ad --- /dev/null +++ b/yazi-core/src/completion/mod.rs @@ -0,0 +1,4 @@ +mod commands; +mod completion; + +pub(super) use completion::*; diff --git a/yazi-core/src/context.rs b/yazi-core/src/context.rs index 86c22800..31546bb3 100644 --- a/yazi-core/src/context.rs +++ b/yazi-core/src/context.rs @@ -1,28 +1,29 @@ use crossterm::terminal::WindowSize; use ratatui::prelude::Rect; -use yazi_config::keymap::KeymapLayer; use yazi_shared::Term; -use crate::{help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}; +use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}; pub struct Ctx { - pub manager: Manager, - pub which: Which, - pub help: Help, - pub input: Input, - pub select: Select, - pub tasks: Tasks, + pub manager: Manager, + pub tasks: Tasks, + pub select: Select, + pub input: Input, + pub help: Help, + pub completion: Completion, + pub which: Which, } impl Ctx { pub fn make() -> Self { Self { - manager: Manager::make(), - which: Default::default(), - help: Default::default(), - input: Default::default(), - select: Default::default(), - tasks: Tasks::start(), + manager: Manager::make(), + tasks: Tasks::start(), + select: Default::default(), + input: Default::default(), + help: Default::default(), + completion: Default::default(), + which: Default::default(), } } @@ -30,29 +31,31 @@ impl Ctx { let WindowSize { columns, rows, .. } = Term::size(); let (x, y) = match pos { - Position::None => return Rect::default(), Position::Top(Rect { mut x, mut y, width, height }) => { x = x.min(columns.saturating_sub(*width)); y = y.min(rows.saturating_sub(*height)); ((columns / 2).saturating_sub(width / 2) + x, y) } - Position::Hovered(rect @ Rect { mut x, y, width, height }) => { - let Some(r) = - self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) - else { - return self.area(&Position::Top(*rect)); - }; - + Position::Sticky(Rect { mut x, y, width, height }, r) => { x = x.min(columns.saturating_sub(*width)); if y + height + r.y + r.height > rows { - (x + r.x, r.y.saturating_sub(height.saturating_sub(1))) + (x + r.x, r.y.saturating_sub(height.saturating_sub(*y))) } else { (x + r.x, y + r.y + r.height) } } + Position::Hovered(rect) => { + return self.area(&if let Some(r) = + self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) + { + Position::Sticky(*rect, r) + } else { + Position::Top(*rect) + }); + } }; - let (w, h) = pos.dimension().unwrap(); + let (w, h) = pos.dimension(); Rect { x, y, width: w.min(columns.saturating_sub(x)), height: h.min(rows.saturating_sub(y)) } } @@ -68,25 +71,8 @@ impl Ctx { None } - #[inline] - pub fn layer(&self) -> KeymapLayer { - if self.which.visible { - KeymapLayer::Which - } else if self.help.visible { - KeymapLayer::Help - } else if self.input.visible { - KeymapLayer::Input - } else if self.select.visible { - KeymapLayer::Select - } else if self.tasks.visible { - KeymapLayer::Tasks - } else { - KeymapLayer::Manager - } - } - #[inline] pub fn image_layer(&self) -> bool { - !matches!(self.layer(), KeymapLayer::Which | KeymapLayer::Help | KeymapLayer::Tasks) + !self.which.visible && !self.help.visible && !self.tasks.visible } } diff --git a/yazi-core/src/input/commands/complete.rs b/yazi-core/src/input/commands/complete.rs new file mode 100644 index 00000000..83d4c68c --- /dev/null +++ b/yazi-core/src/input/commands/complete.rs @@ -0,0 +1,45 @@ +use yazi_config::keymap::Exec; + +use crate::input::Input; + +pub struct Opt<'a> { + word: &'a str, + ticket: usize, +} + +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { + Self { + word: e.args.first().map(|w| w.as_str()).unwrap_or_default(), + ticket: e.named.get("ticket").and_then(|s| s.parse().ok()).unwrap_or(0), + } + } +} + +impl Input { + pub fn complete<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into(); + if self.ticket != opt.ticket { + return false; + } + + let [before, after] = self.partition(); + let new = if let Some((prefix, _)) = before.rsplit_once('/') { + format!("{prefix}/{}{after}", opt.word) + } else { + format!("{}{after}", opt.word) + }; + + let snap = self.snaps.current_mut(); + if new == snap.value { + return false; + } + + let delta = new.chars().count() as isize - snap.value.chars().count() as isize; + snap.value = new; + + self.move_(delta); + self.flush_value(); + true + } +} diff --git a/yazi-core/src/input/commands/mod.rs b/yazi-core/src/input/commands/mod.rs new file mode 100644 index 00000000..df7f200d --- /dev/null +++ b/yazi-core/src/input/commands/mod.rs @@ -0,0 +1 @@ +mod complete; diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 649218a2..d6a7d18d 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -3,23 +3,25 @@ use std::ops::Range; use crossterm::event::KeyCode; use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; -use yazi_config::keymap::Key; +use yazi_config::keymap::{Exec, Key, KeymapLayer}; use yazi_shared::{CharKind, InputError}; use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; -use crate::{external, Position}; +use crate::{emit, external, Position}; #[derive(Default)] pub struct Input { - snaps: InputSnaps, - pub visible: bool, + pub(super) snaps: InputSnaps, + pub ticket: usize, + pub visible: bool, - title: String, + pub title: String, pub position: Position, // Typing - callback: Option>>, - realtime: bool, + callback: Option>>, + realtime: bool, + completion: bool, // Shell pub(super) highlight: bool, @@ -37,6 +39,7 @@ impl Input { // Typing self.callback = Some(tx); self.realtime = opt.realtime; + self.completion = opt.completion; // Shell self.highlight = opt.highlight; @@ -48,6 +51,7 @@ impl Input { _ = cb.send(if submit { Ok(value) } else { Err(InputError::Canceled(value)) }); } + self.ticket = self.ticket.wrapping_add(1); self.visible = false; true } @@ -64,6 +68,10 @@ impl Input { InputMode::Insert => { snap.mode = InputMode::Normal; self.move_(-1); + + if self.completion { + emit!(Call(Exec::call("close", vec![]).vec(), KeymapLayer::Completion)); + } } } self.snaps.tag(); @@ -190,7 +198,8 @@ impl Input { } if let Some(c) = key.plain() { - return self.type_char(c); + let mut bits = [0; 4]; + return self.type_str(c.encode_utf8(&mut bits)); } match key { @@ -199,12 +208,6 @@ impl Input { } } - #[inline] - pub fn type_char(&mut self, c: char) -> bool { - let mut bits = [0; 4]; - self.type_str(c.encode_utf8(&mut bits)) - } - pub fn type_str(&mut self, s: &str) -> bool { let snap = self.snaps.current_mut(); if snap.cursor < 1 { @@ -213,8 +216,9 @@ impl Input { snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s); } + self.move_(s.chars().count() as isize); self.flush_value(); - self.move_(s.chars().count() as isize) + true } pub fn backspace(&mut self) -> bool { @@ -225,8 +229,9 @@ impl Input { snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); } + self.move_(-1); self.flush_value(); - self.move_(-1) + true } pub fn delete(&mut self, cut: bool, insert: bool) -> bool { @@ -278,9 +283,7 @@ impl Input { } self.insert(!before); - for c in s.to_string_lossy().chars() { - self.type_char(c); - } + self.type_str(&s.to_string_lossy()); self.escape(); true } @@ -305,10 +308,6 @@ impl Input { snap.op = InputOp::None; snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; snap.cursor = range.start; - - if self.realtime { - self.callback.as_ref().unwrap().send(Err(InputError::Typed(snap.value.clone()))).ok(); - } } InputOp::Yank(_) => { let range = snap.op.range(cursor, include).unwrap(); @@ -325,24 +324,28 @@ impl Input { return false; } if !matches!(old.op, InputOp::None | InputOp::Select(_)) { - self.snaps.tag(); + self.snaps.tag().then(|| self.flush_value()); } true } #[inline] - fn flush_value(&self) { + pub(super) fn flush_value(&mut self) { + self.ticket = self.ticket.wrapping_add(1); + if self.realtime { let value = self.snap().value.clone(); self.callback.as_ref().unwrap().send(Err(InputError::Typed(value))).ok(); } + + if self.completion { + let before = self.partition()[0].to_owned(); + self.callback.as_ref().unwrap().send(Err(InputError::Completed(before, self.ticket))).ok(); + } } } impl Input { - #[inline] - pub fn title(&self) -> String { self.title.clone() } - #[inline] pub fn value(&self) -> &str { self.snap().slice(self.snap().window()) } @@ -369,6 +372,13 @@ impl Input { Some(s..s + snap.slice(start..end).width() as u16) } + #[inline] + pub fn partition(&self) -> [&str; 2] { + let snap = self.snap(); + let idx = snap.idx(snap.cursor).unwrap(); + [&snap.value[..idx], &snap.value[idx..]] + } + #[inline] fn snap(&self) -> &InputSnap { self.snaps.current() } diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index 9fa10f1a..328d8295 100644 --- a/yazi-core/src/input/mod.rs +++ b/yazi-core/src/input/mod.rs @@ -1,3 +1,4 @@ +mod commands; mod input; mod mode; mod op; @@ -10,6 +11,5 @@ pub use input::*; pub use mode::*; use op::*; pub use option::*; -pub use shell::*; use snap::*; use snaps::*; diff --git a/yazi-core/src/input/option.rs b/yazi-core/src/input/option.rs index dd96c068..f8bb6e64 100644 --- a/yazi-core/src/input/option.rs +++ b/yazi-core/src/input/option.rs @@ -2,35 +2,33 @@ use ratatui::prelude::Rect; use crate::Position; +#[derive(Default)] pub struct InputOpt { - pub title: String, - pub value: String, - pub position: Position, - pub realtime: bool, - pub highlight: bool, + pub title: String, + pub value: String, + pub position: Position, + pub realtime: bool, + pub completion: bool, + pub highlight: bool, } impl InputOpt { pub fn top(title: impl AsRef) -> Self { Self { - 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, + title: title.as_ref().to_owned(), + position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }), + ..Default::default() } } 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(), + position: Position::Hovered( // TODO: hardcode Rect { x: 0, y: 1, width: 50, height: 3 }, ), - realtime: false, - highlight: false, + ..Default::default() } } @@ -46,6 +44,12 @@ impl InputOpt { self } + #[inline] + pub fn with_completion(mut self) -> Self { + self.completion = true; + self + } + #[inline] pub fn with_highlight(mut self) -> Self { self.highlight = true; diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 67db2adf..044a3d2c 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -7,6 +7,7 @@ )] mod blocker; +pub mod completion; mod context; mod event; pub mod external; diff --git a/yazi-core/src/position.rs b/yazi-core/src/position.rs index 68c92a62..37bd0911 100644 --- a/yazi-core/src/position.rs +++ b/yazi-core/src/position.rs @@ -1,23 +1,25 @@ use ratatui::prelude::Rect; -#[derive(Default)] pub enum Position { - #[default] - None, Top(Rect), + Sticky(Rect, Rect), Hovered(Rect), } +impl Default for Position { + fn default() -> Self { Self::Top(Rect::default()) } +} + impl Position { #[inline] - pub fn rect(&self) -> Option { + pub fn rect(&self) -> Rect { match self { - Position::None => None, - Position::Top(rect) => Some(*rect), - Position::Hovered(rect) => Some(*rect), + Position::Top(rect) => *rect, + Position::Sticky(rect, _) => *rect, + Position::Hovered(rect) => *rect, } } #[inline] - pub fn dimension(&self) -> Option<(u16, u16)> { self.rect().map(|r| (r.width, r.height)) } + pub fn dimension(&self) -> (u16, u16) { (self.rect().width, self.rect().height) } } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index 90ec2388..2722a6d0 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -1,6 +1,9 @@ -use std::mem; +use std::{mem, time::Duration}; -use yazi_shared::Url; +use tokio::pin; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; +use yazi_config::keymap::{Exec, KeymapLayer}; +use yazi_shared::{Debounce, InputError, Url}; use crate::{emit, files::{File, FilesOp}, input::InputOpt, tab::Tab}; @@ -59,11 +62,26 @@ 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()))); + let rx = emit!(Input( + InputOpt::top("Change directory:").with_value(target.to_string_lossy()).with_completion() + )); - if let Some(Ok(s)) = result.recv().await { - emit!(Cd(Url::from(s.trim()))); + let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); + pin!(rx); + + while let Some(result) = rx.next().await { + match result { + Ok(s) => { + emit!(Cd(Url::from(s.trim()))); + } + Err(InputError::Completed(before, ticket)) => { + emit!(Call( + Exec::call("complete", vec![]).with("before", before).with("ticket", ticket).vec(), + KeymapLayer::Input + )); + } + _ => break, + } } }); false diff --git a/yazi-fm/src/app.rs b/yazi-fm/src/app.rs index ddf43c8b..3a83a297 100644 --- a/yazi-fm/src/app.rs +++ b/yazi-fm/src/app.rs @@ -59,13 +59,13 @@ impl App { fn dispatch_key(&mut self, key: KeyEvent) { let key = Key::from(key); - if Executor::handle(&mut self.cx, key) { + if Executor::new(&mut self.cx).handle(key) { emit!(Render); } } fn dispatch_paste(&mut self, str: String) { - if self.cx.layer() == KeymapLayer::Input { + if self.cx.input.visible { let input = &mut self.cx.input; if input.mode() == InputMode::Insert && input.type_str(&str) { emit!(Render); @@ -112,7 +112,7 @@ impl App { #[inline] fn dispatch_call(&mut self, exec: Vec, layer: KeymapLayer) { - if Executor::dispatch(&mut self.cx, &exec, layer) { + if Executor::new(&mut self.cx).dispatch(&exec, layer) { emit!(Render); } } diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs new file mode 100644 index 00000000..2f47381f --- /dev/null +++ b/yazi-fm/src/completion/completion.rs @@ -0,0 +1,62 @@ +use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; +use yazi_config::THEME; +use yazi_core::{Ctx, Position}; + +pub(crate) struct Completion<'a> { + cx: &'a Ctx, +} + +impl<'a> Completion<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Completion<'a> { + fn render(self, rect: Rect, buf: &mut Buffer) { + let items = self + .cx + .completion + .window() + .iter() + .enumerate() + .map(|(i, x)| { + let icon = if x.ends_with('/') || x.ends_with('\\') { + &THEME.completion.icon_folder + } else { + &THEME.completion.icon_file + }; + + let mut item = ListItem::new(format!(" {} {}", icon, x)); + if i == self.cx.completion.rel_cursor() { + item = item.style(THEME.completion.active.into()); + } else { + item = item.style(THEME.completion.inactive.into()); + } + + item + }) + .collect::>(); + + let input_area = self.cx.area(&self.cx.input.position); + let mut area = self.cx.area(&Position::Sticky( + Rect { x: 1, y: 0, width: 20, height: items.len() as u16 + 2 }, + input_area, + )); + + if area.y > input_area.y { + area.y = area.y.saturating_sub(1); + } else { + area.y = rect.height.min(area.y + 1); + area.height = rect.height.saturating_sub(area.y).min(area.height); + } + + Clear.render(area, buf); + List::new(items) + .block( + Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(THEME.completion.border.into()), + ) + .render(area, buf); + } +} diff --git a/yazi-fm/src/completion/mod.rs b/yazi-fm/src/completion/mod.rs new file mode 100644 index 00000000..3084e503 --- /dev/null +++ b/yazi-fm/src/completion/mod.rs @@ -0,0 +1,3 @@ +mod completion; + +pub(super) use completion::*; diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 2139fecc..9ad78584 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -2,78 +2,99 @@ use yazi_config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP}; use yazi_core::{emit, input::InputMode, tab::FinderCase, Ctx}; use yazi_shared::{optional_bool, Url}; -pub(super) struct Executor; +pub(super) struct Executor<'a> { + cx: &'a mut Ctx, +} -impl Executor { - pub(super) fn handle(cx: &mut Ctx, key: Key) -> bool { - let layer = cx.layer(); - if layer == KeymapLayer::Which { - return cx.which.press(key); +impl<'a> Executor<'a> { + #[inline] + pub(super) fn new(cx: &'a mut Ctx) -> Self { Self { cx } } + + pub(super) fn handle(&mut self, key: Key) -> bool { + if self.cx.which.visible { + return self.cx.which.press(key); } - - if layer == KeymapLayer::Input && cx.input.type_(&key) { + if self.cx.input.visible && self.cx.input.type_(&key) { + return true; + } + if self.cx.help.visible && self.cx.help.type_(&key) { return true; } - if layer == KeymapLayer::Help && cx.help.type_(&key) { - return true; - } + let b = if self.cx.completion.visible { + self.matches(KeymapLayer::Completion, key).or_else(|| self.matches(KeymapLayer::Input, key)) + } else if self.cx.help.visible { + self.matches(KeymapLayer::Help, key) + } else if self.cx.input.visible { + self.matches(KeymapLayer::Input, key) + } else if self.cx.select.visible { + self.matches(KeymapLayer::Select, key) + } else if self.cx.tasks.visible { + self.matches(KeymapLayer::Tasks, key) + } else { + self.matches(KeymapLayer::Manager, key) + }; + b == Some(true) + } + #[inline] + fn matches(&mut self, layer: KeymapLayer, key: Key) -> Option { for Control { on, exec, .. } in KEYMAP.get(layer) { if on.is_empty() || on[0] != key { continue; } - return if on.len() > 1 { - cx.which.show(&key, layer) + return Some(if on.len() > 1 { + self.cx.which.show(&key, layer) } else { - Self::dispatch(cx, exec, layer) - }; + self.dispatch(exec, layer) + }); } - false + None } #[inline] - pub(super) fn dispatch(cx: &mut Ctx, exec: &[Exec], layer: KeymapLayer) -> bool { + pub(super) fn dispatch(&mut self, exec: &[Exec], layer: KeymapLayer) -> bool { let mut render = false; for e in exec { render |= match layer { - KeymapLayer::Manager => Self::manager(cx, e), - KeymapLayer::Tasks => Self::tasks(cx, e), - KeymapLayer::Select => Self::select(cx, e), - KeymapLayer::Input => Self::input(cx, e), - KeymapLayer::Help => Self::help(cx, e), + KeymapLayer::Manager => self.manager(e), + KeymapLayer::Tasks => self.tasks(e), + KeymapLayer::Select => self.select(e), + KeymapLayer::Input => self.input(e), + KeymapLayer::Help => self.help(e), + KeymapLayer::Completion => self.completion(e), KeymapLayer::Which => unreachable!(), }; } render } - fn manager(cx: &mut Ctx, exec: &Exec) -> bool { + fn manager(&mut self, exec: &Exec) -> bool { match exec.cmd.as_str() { - "escape" => cx.manager.active_mut().escape(exec), - "quit" => cx.manager.quit(&cx.tasks, exec.named.contains_key("no-cwd-file")), - "close" => cx.manager.close(&cx.tasks), - "suspend" => cx.manager.suspend(), + "escape" => self.cx.manager.active_mut().escape(exec), + "quit" => self.cx.manager.quit(&self.cx.tasks, exec.named.contains_key("no-cwd-file")), + "close" => self.cx.manager.close(&self.cx.tasks), + "suspend" => self.cx.manager.suspend(), // Navigation "arrow" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or_default(); - cx.manager.active_mut().arrow(step) + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or_default(); + self.cx.manager.active_mut().arrow(step) } "peek" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - cx.manager.active_mut().preview.arrow(step); - cx.manager.peek(true, cx.image_layer()) + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); + self.cx.manager.active_mut().preview.arrow(step); + self.cx.manager.peek(true, self.cx.image_layer()) } - "leave" => cx.manager.active_mut().leave(), - "enter" => cx.manager.active_mut().enter(), - "back" => cx.manager.active_mut().back(), - "forward" => cx.manager.active_mut().forward(), + "leave" => self.cx.manager.active_mut().leave(), + "enter" => self.cx.manager.active_mut().enter(), + "back" => self.cx.manager.active_mut().back(), + "forward" => self.cx.manager.active_mut().forward(), "cd" => { - let url = exec.args.get(0).map(Url::from).unwrap_or_default(); + let url = exec.args.first().map(Url::from).unwrap_or_default(); if exec.named.contains_key("interactive") { - cx.manager.active_mut().cd_interactive(url) + self.cx.manager.active_mut().cd_interactive(url) } else { emit!(Cd(url)); false @@ -83,64 +104,68 @@ impl Executor { // Selection "select" => { let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); - cx.manager.active_mut().select(optional_bool(&state)) + self.cx.manager.active_mut().select(optional_bool(&state)) } "select_all" => { let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); - cx.manager.active_mut().select_all(optional_bool(&state)) + self.cx.manager.active_mut().select_all(optional_bool(&state)) } - "visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")), + "visual_mode" => self.cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")), // Operation - "open" => cx.manager.open(exec.named.contains_key("interactive")), - "yank" => cx.manager.yank(exec.named.contains_key("cut")), + "open" => self.cx.manager.open(exec.named.contains_key("interactive")), + "yank" => self.cx.manager.yank(exec.named.contains_key("cut")), "paste" => { - let dest = cx.manager.cwd(); - let (cut, ref src) = cx.manager.yanked; + let dest = self.cx.manager.cwd(); + let (cut, ref src) = self.cx.manager.yanked; let force = exec.named.contains_key("force"); - if cut { cx.tasks.file_cut(src, dest, force) } else { cx.tasks.file_copy(src, dest, force) } + if cut { + self.cx.tasks.file_cut(src, dest, force) + } else { + self.cx.tasks.file_copy(src, dest, force) + } } "link" => { - let (cut, ref src) = cx.manager.yanked; + let (cut, ref src) = self.cx.manager.yanked; !cut - && cx.tasks.file_link( + && self.cx.tasks.file_link( src, - cx.manager.cwd(), + self.cx.manager.cwd(), exec.named.contains_key("relative"), exec.named.contains_key("force"), ) } "remove" => { - let targets = cx.manager.selected().into_iter().map(|f| f.url()).collect(); + let targets = self.cx.manager.selected().into_iter().map(|f| f.url()).collect(); let force = exec.named.contains_key("force"); let permanently = exec.named.contains_key("permanently"); - cx.tasks.file_remove(targets, force, permanently) + self.cx.tasks.file_remove(targets, force, permanently) } - "create" => cx.manager.create(exec.named.contains_key("force")), - "rename" => cx.manager.rename(exec.named.contains_key("force")), - "copy" => cx.manager.active().copy(exec.args.get(0).map(|s| s.as_str()).unwrap_or("")), - "shell" => cx.manager.active().shell( - exec.args.get(0).map(|e| e.as_str()).unwrap_or(""), + "create" => self.cx.manager.create(exec.named.contains_key("force")), + "rename" => self.cx.manager.rename(exec.named.contains_key("force")), + "copy" => self.cx.manager.active().copy(exec.args.first().map(|s| s.as_str()).unwrap_or("")), + "shell" => self.cx.manager.active().shell( + exec.args.first().map(|e| e.as_str()).unwrap_or(""), exec.named.contains_key("block"), exec.named.contains_key("confirm"), ), - "hidden" => cx.manager.active_mut().hidden(exec), - "linemode" => cx.manager.active_mut().linemode(exec), - "search" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") { - "rg" => cx.manager.active_mut().search(true), - "fd" => cx.manager.active_mut().search(false), - _ => cx.manager.active_mut().search_stop(), + "hidden" => self.cx.manager.active_mut().hidden(exec), + "linemode" => self.cx.manager.active_mut().linemode(exec), + "search" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") { + "rg" => self.cx.manager.active_mut().search(true), + "fd" => self.cx.manager.active_mut().search(false), + _ => self.cx.manager.active_mut().search_stop(), }, - "jump" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") { - "fzf" => cx.manager.active_mut().jump(true), - "zoxide" => cx.manager.active_mut().jump(false), + "jump" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") { + "fzf" => self.cx.manager.active_mut().jump(true), + "zoxide" => self.cx.manager.active_mut().jump(false), _ => false, }, // Find "find" => { - let query = exec.args.get(0).map(|s| s.as_str()); + let query = exec.args.first().map(|s| s.as_str()); let prev = exec.named.contains_key("previous"); let case = match (exec.named.contains_key("smart"), exec.named.contains_key("insensitive")) { @@ -148,131 +173,160 @@ impl Executor { (_, false) => FinderCase::Sensitive, (_, true) => FinderCase::Insensitive, }; - cx.manager.active_mut().find(query, prev, case) + self.cx.manager.active_mut().find(query, prev, case) } - "find_arrow" => cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")), + "find_arrow" => self.cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")), // Sorting "sort" => { - let b = cx.manager.active_mut().sort(exec); - cx.tasks.precache_size(&cx.manager.current().files); + let b = self.cx.manager.active_mut().sort(exec); + self.cx.tasks.precache_size(&self.cx.manager.current().files); b } // Tabs "tab_create" => { let path = if exec.named.contains_key("current") { - cx.manager.cwd().to_owned() + self.cx.manager.cwd().to_owned() } else { - exec.args.get(0).map(Url::from).unwrap_or_else(|| Url::from("/")) + exec.args.first().map(Url::from).unwrap_or_else(|| Url::from("/")) }; - cx.manager.tabs.create(&path) + self.cx.manager.tabs.create(&path) } "tab_close" => { - let idx = exec.args.get(0).and_then(|i| i.parse().ok()).unwrap_or(0); - cx.manager.tabs.close(idx) + let idx = exec.args.first().and_then(|i| i.parse().ok()).unwrap_or(0); + self.cx.manager.tabs.close(idx) } "tab_switch" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); let rel = exec.named.contains_key("relative"); - cx.manager.tabs.switch(step, rel) + self.cx.manager.tabs.switch(step, rel) } "tab_swap" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - cx.manager.tabs.swap(step) + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); + self.cx.manager.tabs.swap(step) } // Tasks - "tasks_show" => cx.tasks.toggle(), + "tasks_show" => self.cx.tasks.toggle(), // Help - "help" => cx.help.toggle(cx.layer()), + "help" => self.cx.help.toggle(KeymapLayer::Manager), _ => false, } } - fn tasks(cx: &mut Ctx, exec: &Exec) -> bool { + fn tasks(&mut self, exec: &Exec) -> bool { match exec.cmd.as_str() { - "close" => cx.tasks.toggle(), + "close" => self.cx.tasks.toggle(), "arrow" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - if step > 0 { cx.tasks.next() } else { cx.tasks.prev() } + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); + if step > 0 { self.cx.tasks.next() } else { self.cx.tasks.prev() } } - "inspect" => cx.tasks.inspect(), - "cancel" => cx.tasks.cancel(), + "inspect" => self.cx.tasks.inspect(), + "cancel" => self.cx.tasks.cancel(), - "help" => cx.help.toggle(cx.layer()), + "help" => self.cx.help.toggle(KeymapLayer::Tasks), _ => false, } } - fn select(cx: &mut Ctx, exec: &Exec) -> bool { + fn select(&mut self, exec: &Exec) -> bool { match exec.cmd.as_str() { - "close" => cx.select.close(exec.named.contains_key("submit")), + "close" => self.cx.select.close(exec.named.contains_key("submit")), "arrow" => { - let step: isize = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - if step > 0 { cx.select.next(step as usize) } else { cx.select.prev(step.unsigned_abs()) } + let step: isize = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); + if step > 0 { + self.cx.select.next(step as usize) + } else { + self.cx.select.prev(step.unsigned_abs()) + } } - "help" => cx.help.toggle(cx.layer()), + "help" => self.cx.help.toggle(KeymapLayer::Select), _ => false, } } - fn input(cx: &mut Ctx, exec: &Exec) -> bool { + fn input(&mut self, exec: &Exec) -> bool { match exec.cmd.as_str() { - "close" => return cx.input.close(exec.named.contains_key("submit")), - "escape" => return cx.input.escape(), + "close" => return self.cx.input.close(exec.named.contains_key("submit")), + "escape" => return self.cx.input.escape(), "move" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); let in_operating = exec.named.contains_key("in-operating"); - return if in_operating { cx.input.move_in_operating(step) } else { cx.input.move_(step) }; + return if in_operating { + self.cx.input.move_in_operating(step) + } else { + self.cx.input.move_(step) + }; + } + + "complete" => { + return if exec.args.is_empty() { + self.cx.completion.trigger(exec) + } else { + self.cx.input.complete(exec) + }; } _ => {} } - match cx.input.mode() { + match self.cx.input.mode() { InputMode::Normal => match exec.cmd.as_str() { - "insert" => cx.input.insert(exec.named.contains_key("append")), - "visual" => cx.input.visual(), + "insert" => self.cx.input.insert(exec.named.contains_key("append")), + "visual" => self.cx.input.visual(), - "backward" => cx.input.backward(), - "forward" => cx.input.forward(exec.named.contains_key("end-of-word")), + "backward" => self.cx.input.backward(), + "forward" => self.cx.input.forward(exec.named.contains_key("end-of-word")), "delete" => { - cx.input.delete(exec.named.contains_key("cut"), exec.named.contains_key("insert")) + self.cx.input.delete(exec.named.contains_key("cut"), exec.named.contains_key("insert")) } - "yank" => cx.input.yank(), - "paste" => cx.input.paste(exec.named.contains_key("before")), + "yank" => self.cx.input.yank(), + "paste" => self.cx.input.paste(exec.named.contains_key("before")), - "undo" => cx.input.undo(), - "redo" => cx.input.redo(), + "undo" => self.cx.input.undo(), + "redo" => self.cx.input.redo(), - "help" => cx.help.toggle(cx.layer()), + "help" => self.cx.help.toggle(KeymapLayer::Input), _ => false, }, InputMode::Insert => false, } } - fn help(cx: &mut Ctx, exec: &Exec) -> bool { + fn help(&mut self, exec: &Exec) -> bool { match exec.cmd.as_str() { - "close" => cx.help.toggle(cx.layer()), - "escape" => cx.help.escape(), + "close" => self.cx.help.toggle(KeymapLayer::Help), + "escape" => self.cx.help.escape(), "arrow" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - cx.help.arrow(step) + let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); + self.cx.help.arrow(step) } - "filter" => cx.help.filter(), + "filter" => self.cx.help.filter(), _ => false, } } + + fn completion(&mut self, exec: &Exec) -> bool { + match exec.cmd.as_str() { + "trigger" => self.cx.completion.trigger(exec), + "show" => self.cx.completion.show(exec), + "close" => self.cx.completion.close(exec), + + "arrow" => self.cx.completion.arrow(exec), + + "help" => self.cx.help.toggle(KeymapLayer::Completion), + _ => false, + } + } } diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index 0aab7ea0..71590d10 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -33,7 +33,7 @@ impl<'a> Widget for Input<'a> { .border_type(BorderType::Rounded) .border_style(THEME.input.border.into()) .title({ - let mut line = Line::from(input.title()); + let mut line = Line::from(input.title.as_str()); line.patch_style(THEME.input.title.into()); line }), diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index f54e69c9..3690dff0 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -1,6 +1,7 @@ #![allow(clippy::module_inception)] mod app; +mod completion; mod executor; mod help; mod input; diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index bacae721..4dde5b7b 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -2,7 +2,7 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, wid use yazi_core::Ctx; use yazi_plugin::components; -use super::{input, select, tasks, which}; +use super::{completion, input, select, tasks, which}; use crate::help; pub(super) struct Root<'a> { @@ -40,6 +40,10 @@ impl<'a> Widget for Root<'a> { help::Layout::new(self.cx).render(area, buf); } + if self.cx.completion.visible { + completion::Completion::new(self.cx).render(area, buf); + } + if self.cx.which.visible { which::Which::new(self.cx).render(area, buf); } diff --git a/yazi-plugin/src/bindings/mod.rs b/yazi-plugin/src/bindings/mod.rs index 60bbb912..a6ee7ff7 100644 --- a/yazi-plugin/src/bindings/mod.rs +++ b/yazi-plugin/src/bindings/mod.rs @@ -9,6 +9,7 @@ mod tasks; pub use active::*; pub use bindings::*; +#[allow(unused_imports)] pub use files::*; pub use shared::*; pub use tabs::*; diff --git a/yazi-shared/src/errors/input.rs b/yazi-shared/src/errors/input.rs index 792dc7ae..9c0c1c6f 100644 --- a/yazi-shared/src/errors/input.rs +++ b/yazi-shared/src/errors/input.rs @@ -3,6 +3,7 @@ use std::{error::Error, fmt::{self, Display}}; #[derive(Debug)] pub enum InputError { Typed(String), + Completed(String, usize), Canceled(String), } @@ -10,6 +11,7 @@ impl Display for InputError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Typed(text) => write!(f, "Typed error: {text}"), + Self::Completed(text, _) => write!(f, "Completed error: {text}"), Self::Canceled(text) => write!(f, "Canceled error: {text}"), } } diff --git a/yazi-shared/src/term/mod.rs b/yazi-shared/src/term/mod.rs index f95ce14e..1fc2dcdc 100644 --- a/yazi-shared/src/term/mod.rs +++ b/yazi-shared/src/term/mod.rs @@ -1,5 +1,6 @@ +#![allow(clippy::module_inception)] + mod cursor; mod term; -pub use cursor::*; pub use term::*;