From 146606e034298c4bd52ff88585e6e8fad0ec36f7 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Fri, 3 Nov 2023 16:53:15 +0800 Subject: [PATCH] WIP [no ci] --- cspell.json | 2 +- yazi-config/preset/keymap.toml | 4 +- yazi-config/preset/theme.toml | 8 +++- yazi-config/src/theme/theme.rs | 4 ++ yazi-core/src/completion/commands/arrow.rs | 44 +++++++++++++++++++++ yazi-core/src/completion/commands/close.rs | 32 +++++++++++++++ yazi-core/src/completion/commands/mod.rs | 2 + yazi-core/src/completion/completion.rs | 35 ++++------------- yazi-core/src/input/commands/complete.rs | 45 ++++++++++++++++++++++ yazi-core/src/input/commands/mod.rs | 1 + yazi-core/src/input/input.rs | 31 +++++++-------- yazi-core/src/input/mod.rs | 1 + yazi-core/src/tab/commands/cd.rs | 27 ++++++++++--- yazi-fm/src/completion/completion.rs | 28 ++++++++++++-- yazi-fm/src/executor.rs | 19 +++++---- yazi-shared/src/errors/input.rs | 2 + 16 files changed, 219 insertions(+), 66 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/input/commands/complete.rs create mode 100644 yazi-core/src/input/commands/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 31cb5109..7bb81fed 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -244,8 +244,8 @@ keymap = [ { on = [ "" ], exec = "close", desc = "Cancel completion" }, { on = [ "" ], exec = "close --submit", desc = "Submit the completion" }, - { on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" }, - { on = [ "j" ], 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 = "arrow -1", desc = "Move cursor up" }, { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index 4d7f7b22..4a8f023e 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -84,13 +84,19 @@ inactive = {} # : }}} + # : Completion {{{ [completion] border = { fg = "blue" } -active = { fg = "lightcyan" } +active = { bg = "darkgray" } inactive = {} +# Icons +icon_file = "" +icon_folder = "" +icon_command = "" + # : }}} diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 3ee264a3..80d4f02c 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -86,6 +86,10 @@ 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)] diff --git a/yazi-core/src/completion/commands/arrow.rs b/yazi-core/src/completion/commands/arrow.rs new file mode 100644 index 00000000..a7e7e453 --- /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.items.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..b08b90f1 --- /dev/null +++ b/yazi-core/src/completion/commands/close.rs @@ -0,0 +1,32 @@ +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 From for Opt { + fn from(b: bool) -> Self { Self(b) } +} + +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.items[self.cursor].to_owned()]) + .with_bool("apply", true) + .with("ticket", self.ticket) + .vec(), + KeymapLayer::Input + )); + } + + self.cursor = 0; + self.visible = false; + true + } +} diff --git a/yazi-core/src/completion/commands/mod.rs b/yazi-core/src/completion/commands/mod.rs index 545bd455..7e958bd6 100644 --- a/yazi-core/src/completion/commands/mod.rs +++ b/yazi-core/src/completion/commands/mod.rs @@ -1,2 +1,4 @@ +mod arrow; +mod close; mod show; mod trigger; diff --git a/yazi-core/src/completion/completion.rs b/yazi-core/src/completion/completion.rs index 5a7f6668..42c30f29 100644 --- a/yazi-core/src/completion/completion.rs +++ b/yazi-core/src/completion/completion.rs @@ -1,39 +1,20 @@ #[derive(Default)] pub struct Completion { - pub items: Vec, - pub cursor: usize, + pub(super) items: Vec, + pub(super) offset: usize, + pub cursor: usize, pub ticket: usize, pub visible: bool, } impl Completion { - pub fn close(&mut self, submit: bool) -> bool { - self.cursor = 0; - - self.visible = false; - true - } - - pub fn next(&mut self, step: usize) -> bool { - let len = self.items.len(); - if len == 0 { - return false; - } - - let old = self.cursor; - self.cursor = (self.cursor + step).min(len - 1); - - old != self.cursor - } - - pub fn prev(&mut self, step: usize) -> bool { - let old = self.cursor; - self.cursor = self.cursor.saturating_sub(step); - - old != self.cursor + #[inline] + pub fn window(&self) -> &[String] { + let end = (self.offset + self.limit()).min(self.items.len()); + &self.items[self.offset..end] } #[inline] - pub fn selected(&self) -> Option<&String> { self.items.get(self.cursor) } + pub fn limit(&self) -> usize { self.items.len().min(5) } } diff --git a/yazi-core/src/input/commands/complete.rs b/yazi-core/src/input/commands/complete.rs new file mode 100644 index 00000000..168fd227 --- /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(|v| v.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); + 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 2e62fa53..fb3707d5 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -3,17 +3,17 @@ use std::ops::Range; use crossterm::event::KeyCode; use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; -use yazi_config::keymap::{Exec, Key, KeymapLayer}; +use yazi_config::keymap::Key; use yazi_shared::{CharKind, InputError}; use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; -use crate::{emit, external, Position}; +use crate::{external, Position}; #[derive(Default)] pub struct Input { - snaps: InputSnaps, - pub ticket: usize, - pub visible: bool, + pub(super) snaps: InputSnaps, + pub ticket: usize, + pub visible: bool, pub title: String, pub position: Position, @@ -31,7 +31,6 @@ impl Input { pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender>) { self.close(false); self.snaps.reset(opt.value); - self.ticket = self.ticket.wrapping_add(1); self.visible = true; self.title = opt.title; @@ -52,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 } @@ -213,7 +213,7 @@ impl Input { } self.move_(s.chars().count() as isize); - self.flush_value(); + self.flush_value(false); true } @@ -226,7 +226,7 @@ impl Input { } self.move_(-1); - self.flush_value(); + self.flush_value(false); true } @@ -320,26 +320,23 @@ impl Input { return false; } if !matches!(old.op, InputOp::None | InputOp::Select(_)) { - self.snaps.tag().then(|| self.flush_value()); + self.snaps.tag().then(|| self.flush_value(false)); } true } #[inline] - fn flush_value(&mut self) { + pub(super) fn flush_value(&mut self, no_complete: bool) { 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 { - emit!(Call( - Exec::call("complete", vec![self.partition()[0].to_owned()]) - .with("ticket", self.ticket) - .vec(), - KeymapLayer::Input - )); + + if self.completion && !no_complete { + let before = self.partition()[0].to_owned(); + self.callback.as_ref().unwrap().send(Err(InputError::Completed(before, self.ticket))).ok(); } } } diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index 9fa10f1a..397890e7 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; diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index e7fc6b37..f7ec9656 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,12 +62,26 @@ impl Tab { pub fn cd_interactive(&mut self, target: Url) -> bool { tokio::spawn(async move { - let mut result = emit!(Input( + 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![before]).with("ticket", ticket).vec(), + KeymapLayer::Input + )); + } + _ => break, + } } }); false diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index 6b4cb2ef..c6129eaa 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -1,4 +1,5 @@ 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> { @@ -11,8 +12,24 @@ impl<'a> Completion<'a> { impl<'a> Widget for Completion<'a> { fn render(self, rect: Rect, buf: &mut Buffer) { - let items = - self.cx.completion.items.iter().map(|x| ListItem::new(x.as_str())).collect::>(); + let items = self + .cx + .completion + .window() + .iter() + .enumerate() + .map(|(i, x)| { + let mut item = ListItem::new(format!(" {} {}", THEME.completion.icon_file, x)); + + if i == self.cx.completion.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 = @@ -27,7 +44,12 @@ impl<'a> Widget for Completion<'a> { Clear.render(area, buf); List::new(items) - .block(Block::new().borders(Borders::ALL).border_type(BorderType::Rounded)) + .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/executor.rs b/yazi-fm/src/executor.rs index 8133a192..0d20970f 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -267,7 +267,13 @@ impl<'a> Executor<'a> { }; } - "complete" => return self.cx.completion.trigger(exec), + "complete" => { + return if exec.named.contains_key("apply") { + self.cx.input.complete(exec) + } else { + self.cx.completion.trigger(exec) + }; + } _ => {} } @@ -315,16 +321,9 @@ impl<'a> Executor<'a> { match exec.cmd.as_str() { "trigger" => self.cx.completion.trigger(exec), "show" => self.cx.completion.show(exec), - "close" => self.cx.completion.close(exec.named.contains_key("submit")), + "close" => self.cx.completion.close(exec), - "arrow" => { - let step: isize = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); - if step > 0 { - self.cx.completion.next(step as usize) - } else { - self.cx.completion.prev(step.unsigned_abs()) - } - } + "arrow" => self.cx.completion.arrow(exec), "help" => self.cx.help.toggle(KeymapLayer::Completion), _ => false, 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}"), } }