From 94cda7cde1fffcc3da841d5567dc2bd0689a7960 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Sun, 3 Nov 2024 16:51:29 +0300 Subject: [PATCH 1/4] refactor(app_builder): update Arbor struct --- src/common/app_builder.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/common/app_builder.rs b/src/common/app_builder.rs index d0ee989..9a8e6a8 100644 --- a/src/common/app_builder.rs +++ b/src/common/app_builder.rs @@ -24,8 +24,7 @@ pub struct Args { } pub trait AppBuilder { - fn build( - ) -> Pin>> + Send>> + fn build() -> Pin>> + Send>> where Self: Sync, { @@ -38,19 +37,23 @@ pub trait AppBuilder { None }; - Ok(Autocomplete::build( - args.language.clone(), - args.thread_count, - args.max_suggestion, - args.backup, - output, - ) - .await?) + Ok(Arbor { + autocomplete: Autocomplete::build( + args.language.clone(), + args.thread_count, + args.max_suggestion, + args.backup, + output, + ) + .await?, + }) }) } } -pub struct Arbor; +pub struct Arbor { + pub autocomplete: Autocomplete, +} impl AppBuilder for Arbor {} @@ -74,9 +77,9 @@ mod tests { let mut arbor = Arbor::build().await?; let word = "hello".to_string(); - arbor.insert_word(word.clone()).await?; + arbor.autocomplete.insert_word(word.clone()).await?; - let suggestion = arbor.suggest_word("hel").await?; + let suggestion = arbor.autocomplete.suggest_word("hel").await?; assert_eq!(suggestion.iter().nth(0).unwrap().to_owned(), word); @@ -91,4 +94,3 @@ mod tests { return Ok(Args::try_parse_from(itr)?); } } - From 943561e0ba1e23ade7afbe02dbfe9fbb72fb8e5b Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Sun, 3 Nov 2024 16:52:00 +0300 Subject: [PATCH 2/4] feat: add Repl struct --- Cargo.lock | 87 +++++++++++++++++++++++++++++- Cargo.toml | 1 + src/lib.rs | 1 + src/util/repl.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/util/repl.rs diff --git a/Cargo.lock b/Cargo.lock index 1c30bca..11a7372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ version = "0.1.0" dependencies = [ "bincode", "clap", + "crossterm", "dirs", "tokio", ] @@ -170,6 +171,31 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "dirs" version = "4.0.0" @@ -190,6 +216,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -227,9 +263,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libredox" @@ -241,6 +277,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -251,6 +293,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "memchr" version = "2.7.4" @@ -274,6 +322,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -360,6 +409,19 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -386,6 +448,27 @@ dependencies = [ "syn", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/Cargo.toml b/Cargo.toml index debd108..f18f5a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ bincode = "1.3.3" tokio = { version = "1.40.0", features = ["full"] } dirs = { version = "4.0" } clap = { version = "4.5.20", features = ["derive"] } +crossterm = "0.28.1" diff --git a/src/lib.rs b/src/lib.rs index f35e116..d226a0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,4 +7,5 @@ pub mod util { pub mod app_data; pub mod backup; pub mod errors; + pub mod repl; } diff --git a/src/util/repl.rs b/src/util/repl.rs new file mode 100644 index 0000000..05de46f --- /dev/null +++ b/src/util/repl.rs @@ -0,0 +1,138 @@ +use crossterm::{ + cursor, + event::{self, Event, KeyCode, KeyModifiers}, + execute, + style::{Color, Print, ResetColor, SetForegroundColor}, + terminal::{self, Clear, ClearType}, +}; +use std::time::Duration; +use std::{ + error::Error, + io::{self, Write}, +}; + +use crate::common::app_builder::{AppBuilder, Arbor}; + +pub struct Repl { + arbor: Arbor, + input: String, + selected_suggestion: usize, +} + +impl Repl { + pub async fn new() -> Result> { + Ok(Self { + arbor: Arbor::build().await?, + input: "".to_string(), + selected_suggestion: 0, + }) + } + + pub async fn run(&mut self) -> Result<(), Box> { + let mut stdout = io::stdout(); + + terminal::enable_raw_mode()?; + execute!(stdout, terminal::EnterAlternateScreen)?; + + loop { + if event::poll(Duration::from_millis(100))? { + if let Event::Key(event) = event::read()? { + match event.code { + KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => { + break + } + KeyCode::Char(' ') => { + println!("nonono"); + } + KeyCode::Char(c) => { + self.input.push(c); + self.selected_suggestion = 0; + } + KeyCode::Backspace => { + self.input.pop(); + self.selected_suggestion = 0; + } + KeyCode::Enter => { + self.arbor + .autocomplete + .insert_word(self.input.clone()) + .await?; + self.input = "".to_string(); + self.selected_suggestion = 0; + } + KeyCode::Up => { + if self.selected_suggestion > 0 { + self.selected_suggestion -= 1; + } + } + KeyCode::Down => { + self.selected_suggestion += 1; + } + KeyCode::Tab => { + if let Some(suggestion) = self + .arbor + .autocomplete + .suggest_word(self.input.as_str()) + .await? + .get(self.selected_suggestion) + { + self.input = suggestion.to_string() + } + + self.selected_suggestion = 0; + } + KeyCode::Esc => break, + _ => panic!("Unknown keystroke!"), + } + } + } + + let suggestions = self + .arbor + .autocomplete + .suggest_word(self.input.as_str()) + .await?; + let max_index = suggestions.len().saturating_sub(1); + if self.selected_suggestion > max_index { + self.selected_suggestion = max_index; + } + + execute!(stdout, Clear(ClearType::All))?; + + execute!( + stdout, + cursor::MoveTo(0, 0), + Print(format!("> {}", self.input)) + )?; + + for (i, suggestion) in suggestions.iter().enumerate() { + if i == self.selected_suggestion { + execute!( + stdout, + cursor::MoveTo(2, (i + 1) as u16), + SetForegroundColor(Color::Green), + Print(suggestion), + ResetColor + )?; + } else { + execute!( + stdout, + cursor::MoveTo(2, (i + 1) as u16), + SetForegroundColor(Color::DarkGrey), + Print(suggestion), + ResetColor + )?; + } + } + + execute!(stdout, cursor::MoveTo(2 + self.input.len() as u16, 0))?; + + stdout.flush()?; + } + + execute!(stdout, terminal::LeaveAlternateScreen)?; + terminal::disable_raw_mode()?; + + Ok(()) + } +} From 37b3de5a86f6f1dbe950363e9db20cb2120446e5 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Sun, 3 Nov 2024 16:52:09 +0300 Subject: [PATCH 3/4] chore: update main.rs --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index f13b985..67bd85b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use std::error::Error; -use arbor::common::app_builder::{AppBuilder, Arbor}; +use arbor::util::repl::Repl; #[tokio::main] async fn main() -> Result<(), Box> { - Arbor::build().await?; + Repl::new().await?.run().await?; Ok(()) } From f46f4aa12d491dffd29337e3a09872ddac233af3 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Sun, 3 Nov 2024 20:03:47 +0300 Subject: [PATCH 4/4] fix(repl): update keystroke feedback --- src/util/repl.rs | 88 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/src/util/repl.rs b/src/util/repl.rs index 05de46f..5abed65 100644 --- a/src/util/repl.rs +++ b/src/util/repl.rs @@ -16,6 +16,7 @@ use crate::common::app_builder::{AppBuilder, Arbor}; pub struct Repl { arbor: Arbor, input: String, + input_section: usize, selected_suggestion: usize, } @@ -24,6 +25,7 @@ impl Repl { Ok(Self { arbor: Arbor::build().await?, input: "".to_string(), + input_section: 0, selected_suggestion: 0, }) } @@ -38,27 +40,68 @@ impl Repl { if event::poll(Duration::from_millis(100))? { if let Event::Key(event) = event::read()? { match event.code { + KeyCode::Char(' ') => { + if self.input.len() == 0 { + continue; + } + + self.input.push(' '); + self.input_section += 1; + + if self + .input + .chars() + .nth(self.input.len().saturating_sub(2)) + .unwrap() + == ' ' + { + self.input.pop(); + self.input_section -= 1; + } + } KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => { break } - KeyCode::Char(' ') => { - println!("nonono"); - } KeyCode::Char(c) => { self.input.push(c); self.selected_suggestion = 0; } KeyCode::Backspace => { + if self.input.len() == 0 { + continue; + } + + let curr = self + .input + .chars() + .nth(self.input.len().saturating_sub(1)) + .unwrap(); + + if curr == ' ' { + self.input_section -= 1; + } + self.input.pop(); self.selected_suggestion = 0; } KeyCode::Enter => { - self.arbor - .autocomplete - .insert_word(self.input.clone()) - .await?; + let words = self.input.split(' ').collect::>(); + + for word in words { + // word length must be bigger than 1 character + if word.len() < 2 { + continue; + } + + self.arbor + .autocomplete + .insert_word(word.to_string()) + .await?; + } + self.input = "".to_string(); self.selected_suggestion = 0; + self.input_section = 0; } KeyCode::Up => { if self.selected_suggestion > 0 { @@ -69,17 +112,34 @@ impl Repl { self.selected_suggestion += 1; } KeyCode::Tab => { + if self.input.len() == 0 { + continue; + } + if let Some(suggestion) = self .arbor .autocomplete - .suggest_word(self.input.as_str()) + .suggest_word( + self.input + .split(' ') + .collect::>() + .get(self.input_section) + .unwrap(), + ) .await? .get(self.selected_suggestion) { - self.input = suggestion.to_string() + let mut words = self.input.split(' ').collect::>(); + + words[self.input_section] = suggestion.as_str(); + + self.input = words.join(" "); } self.selected_suggestion = 0; + + self.input.push(' '); + self.input_section += 1; } KeyCode::Esc => break, _ => panic!("Unknown keystroke!"), @@ -90,8 +150,16 @@ impl Repl { let suggestions = self .arbor .autocomplete - .suggest_word(self.input.as_str()) + .suggest_word( + self.input + .split(' ') + .collect::>() + .get(self.input_section) + .unwrap(), + ) .await?; + + // NOTE: this is to prevent selection overflow let max_index = suggestions.len().saturating_sub(1); if self.selected_suggestion > max_index { self.selected_suggestion = max_index;