diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-06-07 12:43:35 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-07 12:43:35 +0200 |
commit | c11d75f9157873fc99fe0d40933de8ec5fbb4f6b (patch) | |
tree | fc55dc7f9132395585613dd9cce94c98c8c76600 /default-plugins | |
parent | b8f095330a57c905f23563ca7c2bfae3171abf57 (diff) |
feat(wasm-plugin-system): major overhaul and some goodies (#2510)
* strider resiliency
* worker channel prototype
* finalized ui
* show hide plugin
* fs events to plugins
* tests for events and new screen instructions
* various refactoringz
* report plugin errors instead of crashing zellij
* fix plugin loading with workers
* refactor: move watch filesystem
* some fixes and refactoring
* refactor(panes): combine pane insertion logic
* refactor(screen): launch or focus
* refactor(pty): consolidate default shell fetching
* refactor: various cleanups
* initial refactoring
* more initial refactoring
* refactor(strider): search
* style(fmt): rustfmt
* style(pty): cleanup
* style(clippy): ok clippy
* style(fmt): rustfmt
Diffstat (limited to 'default-plugins')
-rw-r--r-- | default-plugins/fixture-plugin-for-tests/src/main.rs | 6 | ||||
-rw-r--r-- | default-plugins/strider/Cargo.toml | 1 | ||||
-rw-r--r-- | default-plugins/strider/src/main.rs | 191 | ||||
-rw-r--r-- | default-plugins/strider/src/search.rs | 415 | ||||
-rw-r--r-- | default-plugins/strider/src/search/controls_line.rs | 353 | ||||
-rw-r--r-- | default-plugins/strider/src/search/mod.rs | 329 | ||||
-rw-r--r-- | default-plugins/strider/src/search/search_results.rs | 308 | ||||
-rw-r--r-- | default-plugins/strider/src/search/search_state.rs | 241 | ||||
-rw-r--r-- | default-plugins/strider/src/search/selection_controls_area.rs | 61 | ||||
-rw-r--r-- | default-plugins/strider/src/search/ui.rs | 120 | ||||
-rw-r--r-- | default-plugins/strider/src/state.rs | 80 |
11 files changed, 1548 insertions, 557 deletions
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 124b2dc75..7e4139f55 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -32,7 +32,7 @@ impl<'de> ZellijWorker<'de> for TestWorker { } register_plugin!(State); -register_worker!(TestWorker, test_worker); +register_worker!(TestWorker, test_worker, TEST_WORKER); impl ZellijPlugin for State { fn load(&mut self) { @@ -40,6 +40,10 @@ impl ZellijPlugin for State { EventType::InputReceived, EventType::SystemClipboardFailure, EventType::CustomMessage, + EventType::FileSystemCreate, + EventType::FileSystemRead, + EventType::FileSystemUpdate, + EventType::FileSystemDelete, ]); } diff --git a/default-plugins/strider/Cargo.toml b/default-plugins/strider/Cargo.toml index d45a8ff21..65b5d23e9 100644 --- a/default-plugins/strider/Cargo.toml +++ b/default-plugins/strider/Cargo.toml @@ -16,3 +16,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" unicode-width = "0.1.8" ansi_term = "0.12.1" +strip-ansi-escapes = "0.1.1" diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 4f299c508..966b53a4f 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -2,26 +2,45 @@ mod search; mod state; use colored::*; -use search::{ResultsOfSearch, SearchWorker}; +use search::{FileContentsWorker, FileNameWorker, MessageToSearch, ResultsOfSearch}; +use serde::{Deserialize, Serialize}; use serde_json; -use state::{refresh_directory, FsEntry, State, CURRENT_SEARCH_TERM}; +use state::{refresh_directory, FsEntry, State}; use std::{cmp::min, time::Instant}; use zellij_tile::prelude::*; register_plugin!(State); -register_worker!(SearchWorker, search_worker); +register_worker!(FileNameWorker, file_name_search_worker, FILE_NAME_WORKER); +register_worker!( + FileContentsWorker, + file_contents_search_worker, + FILE_CONTENTS_WORKER +); impl ZellijPlugin for State { fn load(&mut self) { refresh_directory(self); - self.loading = true; + self.search_state.loading = true; subscribe(&[ EventType::Key, EventType::Mouse, EventType::CustomMessage, EventType::Timer, + EventType::FileSystemCreate, + EventType::FileSystemUpdate, + EventType::FileSystemDelete, ]); - post_message_to("search", String::from("scan_folder"), String::new()); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + "".to_owned(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + "".to_owned(), + ); + self.search_state.loading = true; set_timeout(0.5); // for displaying loading animation } @@ -35,57 +54,43 @@ impl ZellijPlugin for State { self.ev_history.push_back((event.clone(), Instant::now())); match event { Event::Timer(_elapsed) => { - should_render = true; - if self.loading { + if self.search_state.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); - } + self.search_state.progress_animation(); + should_render = true; } }, - Event::CustomMessage(message, payload) => match message.as_str() { - "update_search_results" => { - if let Ok(mut results_of_search) = - serde_json::from_str::<ResultsOfSearch>(&payload) + Event::CustomMessage(message, payload) => match serde_json::from_str(&message) { + Ok(MessageToPlugin::UpdateFileNameSearchResults) => { + if let Ok(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; - } + self.search_state + .update_file_name_search_results(results_of_search); + should_render = true; + } + }, + Ok(MessageToPlugin::UpdateFileContentsSearchResults) => { + if let Ok(results_of_search) = serde_json::from_str::<ResultsOfSearch>(&payload) + { + self.search_state + .update_file_contents_search_results(results_of_search); + should_render = true; } }, - "done_scanning_folder" => { - self.loading = false; + Ok(MessageToPlugin::DoneScanningFolder) => { + self.search_state.loading = false; should_render = true; }, - _ => {}, + Err(e) => eprintln!("Failed to deserialize custom message: {:?}", e), }, 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()), - ); - } + Key::Esc if self.typing_search_term() => { + self.stop_typing_search_term(); + self.search_state.handle_key(key); should_render = true; }, - Key::Esc if self.exploring_search_results() => { - self.stop_exploring_search_results(); + _ if self.typing_search_term() => { + self.search_state.handle_key(key); should_render = true; }, Key::Char('/') => { @@ -94,40 +99,27 @@ impl ZellijPlugin for State { }, Key::Esc => { self.stop_typing_search_term(); + hide_self(); should_render = true; }, Key::Up | Key::Char('k') => { - if self.exploring_search_results() { - self.move_search_selection_up(); + let currently_selected = self.selected(); + *self.selected_mut() = self.selected().saturating_sub(1); + if currently_selected != self.selected() { 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') => { - if self.exploring_search_results() { - self.move_search_selection_down(); + 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; - } 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(); - } + self.traverse_dir_or_open_file(); + self.ev_history.clear(); should_render = true; }, Key::Left | Key::Char('h') => { @@ -190,6 +182,54 @@ impl ZellijPlugin for State { }, _ => {}, }, + Event::FileSystemCreate(paths) => { + let paths: Vec<String> = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, + Event::FileSystemUpdate(paths) => { + let paths: Vec<String> = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, + Event::FileSystemDelete(paths) => { + let paths: Vec<String> = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, _ => { dbg!("Unknown event {:?}", event); }, @@ -198,8 +238,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); + if self.typing_search_term() { + self.search_state.change_size(rows, cols); + print!("{}", self.search_state); + return; } for i in 0..rows { @@ -221,9 +263,9 @@ impl ZellijPlugin for State { if i == self.selected() { if is_last_row { - print!("{}", path.reversed()); + print!("{}", path.clone().reversed()); } else { - println!("{}", path.reversed()); + println!("{}", path.clone().reversed()); } } else { if is_last_row { @@ -238,3 +280,10 @@ impl ZellijPlugin for State { } } } + +#[derive(Serialize, Deserialize)] +pub enum MessageToPlugin { + UpdateFileNameSearchResults, + UpdateFileContentsSearchResults, + DoneScanningFolder, +} diff --git a/default-plugins/strider/src/search.rs b/default-plugins/strider/src/search.rs deleted file mode 100644 index 299882eac..000000000 --- a/default-plugins/strider/src/search.rs +++ /dev/null @@ -1,415 +0,0 @@ -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, - indices, - max_width, - None, - Some(green_code), - true, - ); - format!("{} | {}{}", green_foreground, reset_code, line) - } else { - let line = - self.render_line_with_indices(path, indices, max_width, None, None, true); - format!(" | {}", line) - } - }, - SearchResult::LineInFile { - path, - line, - line_number, - indices, - .. - } => { - if is_selected { - let first_line = self.render_line_with_indices( - path, - &vec![], - max_width, - None, - Some(green_code), - true, - ); - let line_indication_text = format!("{}-> {}", bold_code, line_number); - let line_indication = format!( - "{}{}{}", - orange_foreground, line_indication_text, reset_code - ); // TODO: also truncate - let second_line = self.render_line_with_indices( - line, - indices, - max_width.saturating_sub(line_indication_text.width()), - None, - Some(orange_code), - false, - ); - format!( - " {}│{} {}\n {}│{} {} {}", - green_foreground, - reset_code, - first_line, - green_foreground, - reset_code, - line_indication, - second_line - ) - } else { - let first_line = - self.render_line_with_indices(path, &vec![], max_width, None, None, true); // TODO: - let line_indication_text = format!("{}-> {}", bold_code, line_number); - let second_line = self.render_line_with_indices( - line, - indices, - max_width.saturating_sub(line_indication_text.width()), - None, - None, - false, - ); - format!( - " │ {}\n │ {} {}", - first_line, line_indication_text, second_line - ) - } - }, - } - } - fn render_line_with_indices( - &self, - line_to_render: &String, - indices: &Vec<usize>, - max_width: usize, - background_color: Option<usize>, - foreground_color: Option<usize>, - is_bold: bool, - ) -> String { - // TODO: get these from Zellij - let reset_code = "\u{1b}[m"; - let underline_code = "\u{1b}[4m"; - let foreground_color = foreground_color - .map(|c| format!("\u{1b}[38;5;{}m", c)) - .unwrap_or_else(|| format!("")); - let background_color = background_color - .map(|c| format!("\u{1b}[48;5;{}m", c)) - .unwrap_or_else(|| format!("")); - let bold = if is_bold { "\u{1b}[1m" } else { "" }; - let non_index_character_style = format!("{}{}{}", background_color, foreground_color, bold); - let index_character_style = format!( - "{}{}{}{}", - background_color, foreground_color, bold, underline_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); - let mut second_half = format!("{}", reset_code); - 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(&index_character_style); - first_half.push(character); - first_half.push_str(reset_code); - } else { - first_half.push_str(&non_index_character_style); - first_half.push(character); - first_half.push_str(reset_code); - } - } else if Some(i) > truncate_end_position { - if indices.contains(&i) { - second_half.push_str(&index_character_style); - second_half.push(character); - second_half.push_str(reset_code); - } else { - second_half.push_str(&non_index_character_style); - second_half.push(character); - second_half.push_str(reset_code); - } - } - } - if let Some(_truncate_start_position) = truncate_start_position { - format!( - "{}{}{}[..]{}{}{}", - first_half, reset_code, foreground_color, reset_code, 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<'de> ZellijWorker<'de> for SearchWorker { - // TODO: handle out of order messages, likely when rendering - fn on_message(&mut self, message: String, payload: String) { - match message.as_str() { - // TODO: deserialize to type - "scan_folder" => { - self.populate_search_paths(); - 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); - }, - }, - _ => {}, - } - } -} - -impl SearchWorker { - 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(cur |