diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-05-16 12:47:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-16 12:47:18 +0200 |
commit | 5fe4d60c220c872c1e77a7ddf24fec6686c28e95 (patch) | |
tree | b6abec3006de7cd2eaecee149005d55ee4671a6b | |
parent | 4b7d7c34b72e67168a2ec53b27776047ccddf522 (diff) |
feat(plugins): Plugin workers and strider (#2449)
* mvp of strider fuzzy find
* improve search ui
* various refactoringz
* moar refactoring
* even more refactoring
* tests and more refactoring
* refactor: remove unused stuff
* style(fmt): rustfmt
* debug ci
* debug ci
* correct path for plugin system tests
* fix plugin system ci tests
* remove debugging statements from test
* fix plugin worker persistence
* add test for plugin worker persistence
* style(fmt): rustfmt
* final cleanups
* remove outdated comments
34 files changed, 1926 insertions, 186 deletions
diff --git a/Cargo.lock b/Cargo.lock index 74374912a..33c762962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,11 +776,12 @@ checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" [[package]] name = "dialoguer" -version = "0.10.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c8ae48e400addc32a8710c8d62d55cb84249a7d58ac4cd959daecfbaddc545" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" dependencies = [ "console", + "shell-words", "tempfile", "zeroize", ] @@ -1020,6 +1021,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] +name = "fixture-plugin-for-tests" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "zellij-tile", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1072,6 +1082,15 @@ dependencies = [ ] [[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] name = "generational-arena" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2444,6 +2463,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2597,6 +2625,12 @@ dependencies = [ ] [[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] name = "shellexpand" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2798,8 +2832,14 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" name = "strider" version = "0.2.0" dependencies = [ + "ansi_term", "colored", + "fuzzy-matcher", "pretty-bytes", + "serde", + "serde_json", + "unicode-width", + "walkdir", "zellij-tile", ] @@ -3067,6 +3107,16 @@ dependencies = [ ] [[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] name = "time" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3372,6 +3422,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4212,6 +4272,7 @@ dependencies = [ "sixel-image", "sixel-tokenizer", "sysinfo", + "tempfile", "typetag", "unicode-width", "url", @@ -4278,6 +4339,7 @@ dependencies = [ "thiserror", "unicode-width", "url", + "uuid", "vte 0.11.0", ] diff --git a/Cargo.toml b/Cargo.toml index 1c16269f2..1ce85c77f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "default-plugins/status-bar", "default-plugins/strider", "default-plugins/tab-bar", + "default-plugins/fixture-plugin-for-tests", "zellij-client", "zellij-server", "zellij-utils", diff --git a/default-plugins/fixture-plugin-for-tests/.cargo/config.toml b/default-plugins/fixture-plugin-for-tests/.cargo/config.toml new file mode 100644 index 000000000..bc255e30b --- /dev/null +++ b/default-plugins/fixture-plugin-for-tests/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi"
\ No newline at end of file diff --git a/default-plugins/fixture-plugin-for-tests/Cargo.toml b/default-plugins/fixture-plugin-for-tests/Cargo.toml new file mode 100644 index 000000000..39211c340 --- /dev/null +++ b/default-plugins/fixture-plugin-for-tests/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fixture-plugin-for-tests" +version = "0.1.0" +authors = ["Aram Drevekenin <aram@poor.dev>"] +edition = "2021" +license = "MIT" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +zellij-tile = { path = "../../zellij-tile" } diff --git a/default-plugins/fixture-plugin-for-tests/LICENSE.md b/default-plugins/fixture-plugin-for-tests/LICENSE.md new file mode 120000 index 000000000..f0608a63a --- /dev/null +++ b/default-plugins/fixture-plugin-for-tests/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md
\ No newline at end of file diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs new file mode 100644 index 000000000..124b2dc75 --- /dev/null +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; +use zellij_tile::prelude::*; + +// This is a fixture plugin used only for tests in Zellij +// it is not (and should not!) be included in the mainline executable +// it's included here for convenience so that it will be built by the CI + +#[derive(Default)] +struct State { + received_events: Vec<Event>, + received_payload: Option<String>, +} + +#[derive(Default, Serialize, Deserialize)] +struct TestWorker { + number_of_messages_received: usize, +} + +impl<'de> ZellijWorker<'de> for TestWorker { + fn on_message(&mut self, message: String, payload: String) { + if message == "ping" { + self.number_of_messages_received += 1; + post_message_to_plugin( + "pong".into(), + format!( + "{}, received {} messages", + payload, self.number_of_messages_received + ), + ); + } + } +} + +register_plugin!(State); +register_worker!(TestWorker, test_worker); + +impl ZellijPlugin for State { + fn load(&mut self) { + subscribe(&[ + EventType::InputReceived, + EventType::SystemClipboardFailure, + EventType::CustomMessage, + ]); + } + + fn update(&mut self, event: Event) -> bool { + match &event { + Event::CustomMessage(message, payload) => { + if message == "pong" { + self.received_payload = Some(payload.clone()); + } + }, + Event::SystemClipboardFailure => { + // this is just to trigger the worker message + post_message_to( + "test", + "ping".to_owned(), + "gimme_back_my_payload".to_owned(), + ); + }, + _ => {}, + } + let should_render = true; + self.received_events.push(event); + should_render + } + + fn render(&mut self, rows: usize, cols: usize) { + if let Some(payload) = self.received_payload.as_ref() { + println!("Payload from worker: {:?}", payload); + } else { + println!( + "Rows: {:?}, Cols: {:?}, Received events: {:?}", + rows, cols, self.received_events + ); + } + } +} diff --git a/default-plugins/strider/Cargo.toml b/default-plugins/strider/Cargo.toml index f7801bfaf..d45a8ff21 100644 --- a/default-plugins/strider/Cargo.toml +++ b/default-plugins/strider/Cargo.toml @@ -10,3 +10,9 @@ license = "MIT" colored = "2.0.0" zellij-tile = { path = "../../zellij-tile" } pretty-bytes = "0.2.2" +walkdir = "2.3.3" +fuzzy-matcher = "0.3.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +unicode-width = "0.1.8" +ansi_term = "0.12.1" diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 6868c6706..4f299c508 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -1,16 +1,28 @@ +mod search; mod state; use colored::*; -use state::{refresh_directory, FsEntry, State}; +use search::{ResultsOfSearch, SearchWorker}; +use serde_json; +use state::{refresh_directory, FsEntry, State, CURRENT_SEARCH_TERM}; use std::{cmp::min, time::Instant}; use zellij_tile::prelude::*; register_plugin!(State); +register_worker!(SearchWorker, search_worker); impl ZellijPlugin for State { fn load(&mut self) { refresh_directory(self); - subscribe(&[EventType::Key, EventType::Mouse]); + self.loading = true; + subscribe(&[ + EventType::Key, + EventType::Mouse, + EventType::CustomMessage, + EventType::Timer, + ]); + post_message_to("search", String::from("scan_folder"), String::new()); + set_timeout(0.5); // for displaying loading animation } fn update(&mut self, event: Event) -> bool { @@ -22,26 +34,101 @@ impl ZellijPlugin for State { }; self.ev_history.push_back((event.clone(), Instant::now())); match event { + Event::Timer(_elapsed) => { + should_render = true; + if self.loading { + set_timeout(0.5); + if self.loading_animation_offset == u8::MAX { + self.loading_animation_offset = 0; + } else { + self.loading_animation_offset = + self.loading_animation_offset.saturating_add(1); + } + } + }, + Event::CustomMessage(message, payload) => match message.as_str() { + "update_search_results" => { + if let Ok(mut results_of_search) = + serde_json::from_str::<ResultsOfSearch>(&payload) + { + if Some(results_of_search.search_term) == self.search_term { + self.search_results = + results_of_search.search_results.drain(..).collect(); + should_render = true; + } + } + }, + "done_scanning_folder" => { + self.loading = false; + should_render = true; + }, + _ => {}, + }, Event::Key(key) => match key { + // modes: + // 1. typing_search_term + // 2. exploring_search_results + // 3. normal + Key::Esc | Key::Char('\n') if self.typing_search_term() => { + self.accept_search_term(); + }, + _ if self.typing_search_term() => { + self.append_to_search_term(key); + if let Some(search_term) = self.search_term.as_ref() { + std::fs::write(CURRENT_SEARCH_TERM, search_term.as_bytes()).unwrap(); + post_message_to( + "search", + String::from("search"), + String::from(&self.search_term.clone().unwrap()), + ); + } + should_render = true; + }, + Key::Esc if self.exploring_search_results() => { + self.stop_exploring_search_results(); + should_render = true; + }, + Key::Char('/') => { + self.start_typing_search_term(); + should_render = true; + }, + Key::Esc => { + self.stop_typing_search_term(); + should_render = true; + }, Key::Up | Key::Char('k') => { - let currently_selected = self.selected(); - *self.selected_mut() = self.selected().saturating_sub(1); - if currently_selected != self.selected() { + if self.exploring_search_results() { + self.move_search_selection_up(); should_render = true; + } else { + let currently_selected = self.selected(); + *self.selected_mut() = self.selected().saturating_sub(1); + if currently_selected != self.selected() { + should_render = true; + } } }, Key::Down | Key::Char('j') => { - let currently_selected = self.selected(); - let next = self.selected().saturating_add(1); - *self.selected_mut() = min(self.files.len().saturating_sub(1), next); - if currently_selected != self.selected() { + if self.exploring_search_results() { + self.move_search_selection_down(); should_render = true; + } else { + let currently_selected = self.selected(); + let next = self.selected().saturating_add(1); + *self.selected_mut() = min(self.files.len().saturating_sub(1), next); + if currently_selected != self.selected() { + should_render = true; + } } }, Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => { + if self.exploring_search_results() { + self.open_search_result(); + } else { + self.traverse_dir_or_open_file(); + self.ev_history.clear(); + } should_render = true; - self.traverse_dir_or_open_file(); - self.ev_history.clear(); }, Key::Left | Key::Char('h') => { if self.path.components().count() > 2 { @@ -111,6 +198,10 @@ impl ZellijPlugin for State { } fn render(&mut self, rows: usize, cols: usize) { + if self.typing_search_term() || self.exploring_search_results() { + return self.render_search(rows, cols); + } + for i in 0..rows { if self.selected() < self.scroll() { *self.scroll_mut() = self.selected(); diff --git a/default-plugins/strider/src/search.rs b/default-plugins/strider/src/search.rs new file mode 100644 index 000000000..299882eac --- /dev/null +++ b/default-plugins/strider/src/search.rs @@ -0,0 +1,415 @@ +use crate::state::{State, CURRENT_SEARCH_TERM, ROOT}; + +use unicode_width::UnicodeWidthStr; +use zellij_tile::prelude::*; + +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use serde::{Deserialize, Serialize}; +use walkdir::WalkDir; + +use std::io::{self, BufRead}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SearchResult { + File { + path: String, + score: i64, + indices: Vec<usize>, + }, + LineInFile { + path: String, + line: String, + line_number: usize, + score: i64, + indices: Vec<usize>, + }, +} + +impl SearchResult { + pub fn new_file_name(score: i64, indices: Vec<usize>, path: String) -> Self { + SearchResult::File { + path, + score, + indices, + } + } + pub fn new_file_line( + score: i64, + indices: Vec<usize>, + path: String, + line: String, + line_number: usize, + ) -> Self { + SearchResult::LineInFile { + path, + score, + indices, + line, + line_number, + } + } + pub fn score(&self) -> i64 { + match self { + SearchResult::File { score, .. } => *score, + SearchResult::LineInFile { score, .. } => *score, + } + } + pub fn rendered_height(&self) -> usize { + match self { + SearchResult::File { .. } => 1, + SearchResult::LineInFile { .. } => 2, + } + } + pub fn render(&self, max_width: usize, is_selected: bool) -> String { + let green_code = 154; + let orange_code = 166; + let bold_code = "\u{1b}[1m"; + let green_foreground = format!("\u{1b}[38;5;{}m", green_code); + let orange_foreground = format!("\u{1b}[38;5;{}m", orange_code); + let reset_code = "\u{1b}[m"; + let max_width = max_width.saturating_sub(3); // for the UI left line separator + match self { + SearchResult::File { path, indices, .. } => { + if is_selected { + let line = self.render_line_with_indices( + path, + |