summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-05-05 17:30:56 +0200
committerAram Drevekenin <aram@poor.dev>2023-05-05 17:30:56 +0200
commitb739b8303e7d8a2f583430f830ba216dd53fbccd (patch)
tree6d669bf944e759766cca07260766c4fb5830e995
parentacb31c5322b1ffb30420cb6171f50a8e5944d013 (diff)
mvp of strider fuzzy find
-rw-r--r--Cargo.lock44
-rw-r--r--default-plugins/strider/Cargo.toml6
-rw-r--r--default-plugins/strider/src/main.rs129
-rw-r--r--default-plugins/strider/src/search.rs300
-rw-r--r--default-plugins/strider/src/state.rs83
-rw-r--r--zellij-server/src/logging_pipe.rs3
-rw-r--r--zellij-server/src/plugins/mod.rs28
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs92
-rw-r--r--zellij-server/src/plugins/plugin_map.rs63
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs71
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs58
-rw-r--r--zellij-tile/src/lib.rs31
-rw-r--r--zellij-tile/src/shim.rs24
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin490119 -> 796375 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin621069 -> 921497 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin503181 -> 908672 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin458835 -> 765594 bytes
-rw-r--r--zellij-utils/src/data.rs4
-rw-r--r--zellij-utils/src/errors.rs2
19 files changed, 907 insertions, 31 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 55f2bca7d..64381f371 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1042,6 +1042,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"
@@ -2386,6 +2395,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"
@@ -2740,8 +2758,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",
]
@@ -3004,6 +3028,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"
@@ -3309,6 +3343,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"
diff --git a/default-plugins/strider/Cargo.toml b/default-plugins/strider/Cargo.toml
index f7801bfaf..dcfc4f169 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 = "*" # TODO: version number!
+serde_json = "*" # TODO: no!! from zellij-tile!
+unicode-width = "*" # TODO: version number!
+ansi_term = "*" # TODO: version number!
diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs
index 6868c6706..f1a69d065 100644
--- a/default-plugins/strider/src/main.rs
+++ b/default-plugins/strider/src/main.rs
@@ -1,16 +1,47 @@
+// TODO:
+// 1. worker to different file - DONE
+// 2. separate search rendering to different rendering function - DONE
+// 3. make hd scanning happen on startup and show loading indication - DONE
+// 4. make selection and opening files work - TODO: CONTINUE HERE (04/05)
mod state;
+mod search;
use colored::*;
-use state::{refresh_directory, FsEntry, State};
+use state::{refresh_directory, FsEntry, State, CURRENT_SEARCH_TERM};
+use search::{SearchWorker, ResultsOfSearch};
use std::{cmp::min, time::Instant};
use zellij_tile::prelude::*;
+use serde_json;
register_plugin!(State);
+thread_local! {
+ static SEARCH_WORKER: std::cell::RefCell<SearchWorker> = std::cell::RefCell::new(SearchWorker::new());
+}
+
+#[no_mangle]
+pub fn search_worker() {
+ let mut json = String::new();
+ std::io::stdin().read_line(&mut json).unwrap();
+ let (message, payload): (String, String) = serde_json::from_str(&json).unwrap(); // TODO: no unwrap
+ SEARCH_WORKER.with(|search_worker| {
+ search_worker.borrow_mut().on_message(message, payload);
+ });
+}
+
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,
+ ]);
+ eprintln!("post_message_to search scan_folder");
+ 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 +53,97 @@ impl ZellijPlugin for State {
};
self.ev_history.push_back((event.clone(), Instant::now()));
match event {
+ Event::Timer(_elapsed) => {
+ // eprintln!("got timer event");
+ 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" => {
+ eprintln!("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 +213,11 @@ 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..c7cc021dd
--- /dev/null
+++ b/default-plugins/strider/src/search.rs
@@ -0,0 +1,300 @@
+use crate::state::{State, ROOT, CURRENT_SEARCH_TERM};
+
+use zellij_tile::prelude::*;
+use unicode_width::UnicodeWidthStr;
+
+use walkdir::WalkDir;
+use fuzzy_matcher::FuzzyMatcher;
+use fuzzy_matcher::skim::SkimMatcherV2;
+use serde::{Serialize, Deserialize};
+
+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 {
+ match self {
+ SearchResult::File { path, indices, .. } => {
+ self.render_line_with_indices(path, indices, max_width, true, is_selected)
+ }
+ SearchResult::LineInFile { path, line, line_number, indices, .. } => {
+ let rendered_path = self.render_line_with_indices(path, &vec![], max_width, true, is_selected); // TODO:
+ // better
+ let line_indication_text = format!("LINE {}", line_number); // TODO: also truncate
+ // this
+ let rendered_file_line = self.render_line_with_indices(line, indices, max_width.saturating_sub(line_indication_text.width()), false, is_selected);
+ format!("{}\n{} {}", rendered_path, line_indication_text, rendered_file_line)
+ }
+ }
+ }
+ fn render_line_with_indices(&self, line_to_render: &String, indices: &Vec<usize>, max_width: usize, is_file_name: bool, is_selected: bool) -> String {
+ // TODO: get these from Zellij
+ let (green_code, orange_code) = if is_selected {
+ ("\u{1b}[48;5;102;38;5;154;1m", "\u{1b}[48;5;102;38;5;166;1m" )
+ } else {
+ ("\u{1b}[38;5;154;1m", "\u{1b}[38;5;166;1m" )
+ };
+
+ let green_bold_background_code = "\u{1b}[48;5;154;38;5;0;1m";
+ let orange_bold_background_code = "\u{1b}[48;5;166;38;5;0;1m";
+ let reset_code = "\u{1b}[m";
+ let (line_color, index_color) = if is_file_name {
+ (orange_code, orange_bold_background_code)
+ } else {
+ (green_code, green_bold_background_code)
+ };
+ let mut truncate_start_position = None;
+ let mut truncate_end_position = None;
+ if line_to_render.width() > max_width {
+ let length_of_each_half = max_width.saturating_sub(4) / 2;
+ truncate_start_position = Some(length_of_each_half);
+ truncate_end_position = Some(line_to_render.width().saturating_sub(length_of_each_half));
+ }
+ let mut first_half = format!("{}{}", reset_code, line_color);
+ let mut second_half = format!("{}{}", reset_code, line_color);
+ for (i, character) in line_to_render.chars().enumerate() {
+ if (truncate_start_position.is_none() && truncate_end_position.is_none()) || Some(i) < truncate_start_position {
+ if indices.contains(&i) {
+ first_half.push_str(&format!("{}{}{}{}", index_color, character, reset_code, line_color)); // TODO: less allocations
+ } else {
+ first_half.push(character);
+ }
+ } else if Some(i) > truncate_end_position {
+ if indices.contains(&i) {
+ second_half.push_str(&format!("{}{}{}{}", index_color, character, reset_code, line_color)); // TODO: less allocations
+ } else {
+ second_half.push(character);
+ }
+ }
+ }
+ if let Some(_truncate_start_position) = truncate_start_position {
+ format!("{}[..]{}{}", first_half, second_half, reset_code)
+ } else {
+ format!("{}{}", first_half, reset_code)
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ResultsOfSearch {
+ pub search_term: String,
+ pub search_results: Vec<SearchResult>,
+}
+
+impl ResultsOfSearch {
+ pub fn new(search_term: String, search_results: Vec<SearchResult>) -> Self {
+ ResultsOfSearch {
+ search_term,
+ search_results,
+ }
+ }
+ pub fn limit_search_results(mut self, max_results: usize) -> Self {
+ self.search_results.sort_by(|a, b| b.score().cmp(&a.score()));
+ self.search_results = if self.search_results.len() > max_results {
+ self.search_results.drain(..max_results).collect()
+ } else {
+ self.search_results.drain(..).collect()
+ };
+ self
+ }
+}
+
+#[derive(Default, Serialize, Deserialize)]
+pub struct SearchWorker {
+ pub search_paths: Vec<String>,
+ pub search_file_contents: Vec<(String, usize, String)>, // file_name, line_number, line
+ skip_hidden_files: bool,
+}
+
+impl SearchWorker {
+ pub fn new() -> Self {
+ SearchWorker {
+ search_paths: vec![],
+ search_file_contents: vec![],
+ skip_hidden_files: true,
+ }
+ }
+ pub fn on_message(&mut self, message: String, payload: String) {
+ eprintln!("got message: {:?}, search_paths.len: {:?}", message, self.search_paths.len());
+ match message.as_str() { // TODO: deserialize to type
+ "scan_folder" => {
+ if let Err(e) = std::fs::remove_file("/data/search_data") {
+ eprintln!("Warning: failed to remove cache file: {:?}", e);
+ }
+ self.populate_search_paths();
+ eprintln!("search_paths length after populating in scan_folder: {:?}", self.search_paths.len());
+ post_message_to_plugin("done_scanning_folder".into(), "".into());
+ }
+ "search" => {
+ let search_term = payload;
+ let (search_term, matches) = self.search(search_term);
+ let search_results = ResultsOfSearch::new(search_term, matches).limit_search_results(100);
+ post_message_to_plugin("update_search_results".into(), serde_json::to_string(&search_results).unwrap());
+ }
+ "skip_hidden_files" => {
+ match serde_json::from_str::<bool>(&payload) {
+ Ok(should_skip_hidden_files) => {
+ self.skip_hidden_files = should_skip_hidden_files;
+ },
+ Err(e) => {
+ eprintln!("Failed to deserialize payload: {:?}", e);
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ fn search(&mut self, search_term: String) -> (String, Vec<SearchResult>) {
+ if self.search_paths.is_empty() {
+ self.populate_search_paths();
+ }
+ let mut matches = vec![];
+ let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(100); // TODO: no hard
+ // coded limit!
+ self.search_file_names(&search_term, &mut matcher, &mut matches);
+ self.search_file_contents(&search_term, &mut matcher, &mut matches);
+
+ // if the search term changed before we finished, let's search again!
+ if let Ok(current_search_term) = std::fs::read(CURRENT_SEARCH_TERM) {
+ let current_search_term = String::from_utf8_lossy(&current_search_term); // TODO: not lossy, search can be lots of stuff
+ if current_search_term != search_term {
+ return self.search(current_search_term.into());
+ }
+ }
+ (search_term, matches)
+ }
+ fn populate_search_paths(&mut self) {
+ // TODO: CONTINUE HERE - when we start, check to see if /data/search_data exists, if it is
+ // deserialize it and place it in our own state, if not, do the below and then write to it
+ eprintln!("populate_search_paths");
+ if let Ok(search_data) = std::fs::read("/data/search_data") { // TODO: add cwd to here
+ eprintln!("found search data");
+ if let Ok(mut existing_state) = serde_json::from_str::<Self>(&String::from_utf8_lossy(&search_data)) {
+ eprintln!("successfully deserialized search data");
+ std::mem::swap(self, &mut existing_state);
+ return;
+ }
+ }
+ for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) {
+ if self.skip_hidden_files && entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false) {
+ continue;
+ }
+ let file_path = entry.path().display().to_string();
+
+ if entry.metadata().unwrap().is_file() {
+ if let Ok(file) = std::fs::File::open(&file_path) {
+ let lines = io::BufReader::new(file).lines();
+ for (index, line) in lines.enumerate() {
+ match line {
+ Ok(line) => {
+ self.search_file_contents.push((file_path.clone(), index + 1, line));
+ },
+ Err(_) => {
+ break; // probably a binary file, skip it
+ }
+ }
+ }
+ }
+ }
+
+ self.search_paths.push(file_path);
+ }
+ let serialized_state = serde_json::to_string(&self).unwrap(); // TODO: unwrap city
+ std::fs::write("/data/search_data", serialized_state.as_bytes()).unwrap();
+ if let Ok(search_data) = std::fs::read("/data/search_data") {
+ if let Ok(mut existing_state) = serde_json::from_str::<Self>(&String::from_utf8_lossy(&search_data)) {
+ std::mem::swap(self, &mut existing_state);
+ return;
+ }
+ }
+ }
+ fn search_file_names(&self, search_term: &str, matcher: &mut SkimMatcherV2, matches: &mut Vec<SearchResult>) {
+ for entry in &self.search_paths {
+ if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) {
+ matches.push(SearchResult::new_file_name(score, indices, entry.to_owned()));
+ }
+ }
+ }
+ fn search_file_contents(&self, search_term: &str, matcher: &mut SkimMatcherV2, matches: &mut Vec<SearchResult>) {
+ for (file_name, line_number, line_entry) in &self.search_file_contents {
+ if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) {
+ matches.push(SearchResult::new_file_line(score, indices, file_name.clone(), line_entry.clone(), *line_number));
+ }
+ }
+ }
+}
+
+impl State {
+ pub fn render_search(&mut self, rows: usize, cols: usize) {
+ if let Some(search_term) = self.search_term.as_ref() {
+ let mut to_render = String::new();
+ to_render.push_str(&format!("\u{1b}[38;5;51;1mSEARCH:\u{1b}[m {}\n", search_term));
+ let mut rows_left_to_render = rows.saturating_sub(3);
+ if self.loading && self.search_results.is_empty() {
+ to_render.push_str(&self.render_loading());
+ }
+ for (i, result) in self.search_results.iter().enumerate().take(rows.saturating_sub(3)) {
+ let result_height = result.rendered_height();
+ if result_height > rows_left_to_render {
+ break;
+ }
+ rows_left_to_render -= result_height;
+ let is_selected = i == self.selected_search_result;
+ let rendered_result = result.render(cols, is_selected);
+ to_render.push_str(&format!("\n{}", rendered_result));
+ }
+ print!("{}", to_render);
+ }
+ }
+ pub fn render_loading(&self) -> String {
+ let mut rendered = String::from("Scanning folder");
+ let dot_count = self.loading_animation_offset % 4;
+ for _ in 0..dot_count {
+ rendered.push('.');
+ }
+ rendered
+ }
+}
diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs
index 05ede0df7..8c8ed7a82 100644
--- a/