aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-06-07 12:43:35 +0200
committerGitHub <noreply@github.com>2023-06-07 12:43:35 +0200
commitc11d75f9157873fc99fe0d40933de8ec5fbb4f6b (patch)
treefc55dc7f9132395585613dd9cce94c98c8c76600 /default-plugins
parentdocs(changelog): fix cwd of newtab action (diff)
downloadsl0ppij-c11d75f9157873fc99fe0d40933de8ec5fbb4f6b.tar.gz
sl0ppij-c11d75f9157873fc99fe0d40933de8ec5fbb4f6b.tar.xz
sl0ppij-c11d75f9157873fc99fe0d40933de8ec5fbb4f6b.zip
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.rs6
-rw-r--r--default-plugins/strider/Cargo.toml1
-rw-r--r--default-plugins/strider/src/main.rs191
-rw-r--r--default-plugins/strider/src/search.rs415
-rw-r--r--default-plugins/strider/src/search/controls_line.rs353
-rw-r--r--default-plugins/strider/src/search/mod.rs329
-rw-r--r--default-plugins/strider/src/search/search_results.rs308
-rw-r--r--default-plugins/strider/src/search/search_state.rs241
-rw-r--r--default-plugins/strider/src/search/selection_controls_area.rs61
-rw-r--r--default-plugins/strider/src/search/ui.rs120
-rw-r--r--default-plugins/strider/src/state.rs80
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 124b2dc7..7e4139f5 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 d45a8ff2..65b5d23e 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 4f299c50..966b53a4 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 299882ea..00000000
--- 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(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) {
- 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);
- }
- }
- 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 + 1 > rows_left_to_render {
- break;
- }
- rows_left_to_render -= result_height;
- rows_left_to_render -= 1; // space between
- let is_selected = i == self.selected_search_result;
- let rendered_result = result.render(cols, is_selected);
- to_render.push_str(&format!("\n{}\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/search/controls_line.rs b/default-plugins/strider/src/search/controls_line.rs
new file mode 100644
index 00000000..a75715ef
--- /dev/null
+++ b/default-plugins/strider/src/search/controls_line.rs
@@ -0,0 +1,353 @@
+use crate::search::search_state::SearchType;
+use crate::search::ui::{
+ arrow, bold, color_line_to_end, dot, styled_text, BLACK, GRAY_DARK, GRAY_LIGHT, RED, WHITE,
+};
+
+#[derive(Default)]
+pub struct ControlsLine {
+ controls: Vec<Control>,
+ scanning_indication: Option<Vec<&'static str>>,
+ animation_offset: u8,
+}
+
+impl ControlsLine {
+ pub fn new(controls: Vec<Control>, scanning_indication: Option<Vec<&'static str>>) -> Self {
+ ControlsLine {
+ controls,
+ scanning_indication,
+ ..Default::default()
+ }
+ }
+ pub fn with_animation_offset(mut self, animation_offset: u8) -> Self {
+ self.animation_offset = animation_offset;
+ self
+ }
+ pub fn render(&self, max_width: usize, show_controls: bool) -> String {
+ if show_controls {
+ self.render_controls(max_width)
+ } else {
+ self.render_empty_line(max_width)
+ }
+ }
+ pub fn render_controls(&self, max_width: usize) -> String {
+ let loading_animation =
+ LoadingAnimation::new(&self.scanning_indication, self.animation_offset);
+ let full_length = loading_animation.full_len()
+ + self.controls.iter().map(|c| c.full_len()).sum::<usize>();
+ let mid_length =
+ loading_animation.mid_len() + self.controls.iter().map(|c| c.mid_len()).sum::<usize>();
+ let short_length = loading_animation.short_len()
+ + self.controls.iter().map(|c| c.short_len()).sum::<usize>();
+ if max_width >= full_length {
+ let mut to_render = String::new();
+ for control in &self.controls {
+ to_render.push_str(&control.render_full_length());
+ }
+ to_render.push_str(&self.render_padding(max_width.saturating_sub(full_length)));
+ to_render.push_str(&loading_animation.render_full_length());
+ to_render
+ } else if max_width >= mid_length {
+ let mut to_render = String::new();
+ for control in &self.controls {
+ to_render.push_str(&control.render_mid_length());
+ }
+ to_render.push_str(&self.render_padding(max_width.saturating_sub(mid_length)));
+ to_render.push_str(&loading_animation.render_mid_length());
+ to_render
+ } else if max_width >= short_length {
+ let mut to_render = String::new();
+ for control in &self.controls {
+ to_render.push_str(&control.render_short_length());
+ }
+ to_render.push_str(&self.render_padding(max_width.saturating_sub(short_length)));
+ to_render.push_str(&loading_animation.render_short_length());
+ to_render
+ } else {
+ format!("")
+ }
+ }
+ pub fn render_empty_line(&self, max_width: usize) -> String {
+ let loading_animation =
+ LoadingAnimation::new(&self.scanning_indication, self.animation_offset);
+ let mut to_render = String::new();
+ if max_width >= loading_animation.full_len() {
+ to_render.push_str(
+ &self.render_padding(max_width.saturating_sub(loading_animation.full_len())),
+ );
+ to_render.push_str(&loading_animation.render_full_length());
+ } else if max_width >= loading_animation.mid_len() {
+ to_render.push_str(
+ &self.render_padding(max_width.saturating_sub(loading_animation.mid_len())),
+ );
+ to_render.push_str(&loading_animation.render_mid_length());
+ } else if max_width >= loading_animation.short_len() {
+ to_render.push_str(
+ &self.render_padding(max_width.saturating_sub(loading_animation.short_len())),
+ );
+ to_render.push_str(&loading_animation.render_short_length());
+ }
+ to_render
+ }
+ fn render_padding(&self, padding: usize) -> String {
+ // TODO: color whole line
+ format!("{}\u{1b}[{}C", color_line_to_end(GRAY_LIGHT), padding)
+ }
+}
+
+pub struct Control {
+ key: &'static str,
+ options: Vec<&'static str>,
+ option_index: (usize, usize), // eg. 1 out of 2 (1, 2)
+ keycode_background_color: u8,
+ keycode_foreground_color: u8,
+ control_text_background_color: u8,
+ control_text_foreground_color: u8,
+ active_dot_color: u8,
+}
+
+impl Default for Control {
+ fn default() -> Self {
+ Control {
+ key: "",
+ options: vec![],
+ option_index: (0, 0),
+ keycode_background_color: GRAY_LIGHT,
+ keycode_foreground_color: WHITE,
+ control_text_background_color: GRAY_DARK,
+ control_text_foreground_color: BLACK,
+ active_dot_color: RED,
+ }
+ }
+}
+
+impl Control {
+ pub fn new(
+ key: &'static str,
+ options: Vec<&'static str>,
+ option_index: (usize, usize),
+ ) -> Self {
+ Control {
+ key,
+ options,
+ option_index,
+ ..Default::default()
+ }
+ }
+ pub fn new_floating_control(key: &'static str, should_open_floating: bool) -> Self {
+ if should_open_floating {
+ Control::new(key, vec!["OPEN FLOATING", "FLOATING", "F"], (2, 2))
+ } else {
+ Control::new(key, vec!["OPEN TILED", "TILED", "T"], (1, 2))
+ }
+ }
+ pub fn new_filter_control(key: &'static str, search_filter: &SearchType) -> Self {
+ match search_filter {
+ SearchType::NamesAndContents => Control::new(
+ key,
+ vec!["FILE NAMES AND CONTENTS", "NAMES + CONTENTS", "N+C"],
+ (1, 3),
+ ),
+ SearchType::Names => Control::new(key, vec!["FILE NAMES", "NAMES", "N"], (2, 3)),
+ SearchType::Contents => {
+ Control::new(key, vec!["FILE CONTENTS", "CONTENTS", "C"], (3, 3))
+ },
+ }
+ }
+ pub fn short_len(&self) -> usize {
+ let short_text = self
+ .options
+ .get(2)
+ .or_else(|| self.options.get(1))
+ .or_else(|| self.options.get(0))
+ .unwrap_or(&"");
+ short_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
+ // 7 for all the spaces and decorations
+ }
+ pub fn mid_len(&self) -> usize {
+ let mid_text = self
+ .options
+ .get(1)
+ .or_else(|| self.options.get(0))
+ .unwrap_or(&"");
+ mid_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
+ // 7 for all the spaces and decorations
+ }
+ pub fn full_len(&self) -> usize {
+ let full_text = self.options.get(0).unwrap_or(&"");
+ full_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
+ // 7 for all the spaces and decorations
+ }
+ pub fn render_short_length(&self) -> String {
+ let short_text = self
+ .options
+ .get(2)
+ .or_else(|| self.options.get(1))
+ .or_else(|| self.options.get(0))
+ .unwrap_or(&"");
+ self.render(short_text)
+ }
+ pub fn render_mid_length(&self) -> String {
+ let mid_text = self
+ .options
+ .get(1)
+ .or_else(|| self.options.get(0))
+ .unwrap_or(&"");
+ self.render(mid_text)
+ }
+ pub fn render_full_length(&self) -> String {
+ let full_text = self.options.get(0).unwrap_or(&"");
+ self.render(full_text)
+ }
+ fn render(&self, text: &str) -> String {
+ format!(
+ "{}{}{}{}{}{}",
+ self.render_keycode(&format!(" {} ", self.key)),
+ arrow(
+ self.keycode_background_color,
+ self.control_text_background_color
+ ),
+ self.render_selection_dots(),
+ self.render_control_text(&format!("{} ", text)),
+ arrow(
+ self.control_text_background_color,
+ self.keycode_background_color
+ ),
+ color_line_to_end(self.keycode_background_color),
+ )
+ }
+ fn render_keycode(&self, text: &str) -> String {
+ styled_text(
+ self.keycode_foreground_color,
+ self.keycode_background_color,
+ &bold(text),
+ )
+ }
+ fn render_control_text(&self, text: &str) -> String {
+ styled_text(
+ self.control_text_foreground_color,
+ self.control_text_background_color,
+ &bold(text),
+ )
+ }
+ fn render_selection_dots(&self) -> String {
+ let mut selection_dots = String::from(" ");
+ for i in 1..=self.option_index.1 {
+ if i == self.option_index.0 {
+ selection_dots.push_str(&dot(
+ self.active_dot_color,
+ self.control_text_background_color,
+ ));
+ } else {
+ selection_dots.push_str(&dot(
+ self.control_text_foreground_color,
+ self.control_text_background_color,
+ ));
+ }
+ }
+ selection_dots.push_str(" ");
+ selection_dots
+ }
+}
+
+struct LoadingAnimation {
+ scanning_indication: Option<Vec<&'static str>>,
+ animation_offset: u8,
+ background_color: u8,
+ foreground_color: u8,
+}
+impl LoadingAnimation {
+ pub fn new(scanning_indication: &Option<Vec<&'static str>>, animation_offset: u8) -> Self {
+ LoadingAnimation {
+ scanning_indication: scanning_indication.clone(),
+ animation_offset,
+ background_color: GRAY_LIGHT,
+ foreground_color: WHITE,
+ }
+ }
+ pub fn full_len(&self) -> usize {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| scanning_indication.get(0))
+ .map(|s| s.chars().count() + 3) // 3 for animation dots
+ .unwrap_or(0)
+ }
+ pub fn mid_len(&self) -> usize {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| {
+ scanning_indication
+ .get(1)
+ .or_else(|| scanning_indication.get(0))
+ })
+ .map(|s| s.chars().count() + 3) // 3 for animation dots
+ .unwrap_or(0)
+ }
+ pub fn short_len(&self) -> usize {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| {
+ scanning_indication
+ .get(2)
+ .or_else(|| scanning_indication.get(1))
+ .or_else(|| scanning_indication.get(0))
+ })
+ .map(|s| s.chars().count() + 3) // 3 for animation dots
+ .unwrap_or(0)
+ }
+ pub fn render_full_length(&self) -> String {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| scanning_indication.get(0))
+ .map(|s| {
+ styled_text(
+ self.foreground_color,
+ self.background_color,
+ &bold(&(s.to_string() + &self.animation_dots())),
+ )
+ })
+ .unwrap_or_else(String::new)
+ }
+ pub fn render_mid_length(&self) -> String {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| {
+ scanning_indication
+ .get(1)
+ .or_else(|| scanning_indication.get(0))
+ })
+ .map(|s| {
+ styled_text(
+ self.background_color,
+ self.foreground_color,
+ &bold(&(s.to_string() + &self.animation_dots())),
+ )
+ })
+ .unwrap_or_else(String::new)
+ }
+ pub fn render_short_length(&self) -> String {
+ self.scanning_indication
+ .as_ref()
+ .and_then(|scanning_indication| {
+ scanning_indication
+ .get(2)
+ .or_else(|| scanning_indication.get(1))
+ .or_else(|| scanning_indication.get(0))
+ })
+ .map(|s| {
+ styled_text(
+ self.background_color,
+ self.foreground_color,
+ &bold(&(s.to_string() + &self.animation_dots())),
+ )
+ })
+ .unwrap_or_else(String::new)
+ }
+ fn animation_dots(&self) -> String {
+ let mut to_render = String::from("");
+ let dot_count = self.animation_offset % 4;
+ for _ in 0..dot_count {
+ to_render.push('.');
+ }
+ to_render
+ }
+}
diff --git a/default-plugins/strider/src/search/mod.rs b/default-plugins/strider/src/search/mod.rs
new file mode 100644
index 00000000..5aba9c4d
--- /dev/null
+++ b/default-plugins/strider/src/search/mod.rs
@@ -0,0 +1,329 @@
+pub mod controls_line;
+pub mod search_results;
+pub mod search_state;
+pub mod selection_controls_area;
+pub mod ui;
+
+use crate::state::{CURRENT_SEARCH_TERM, ROOT};
+use crate::MessageToPlugin;
+use search_state::SearchType;
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::path::{Path, PathBuf};
+
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+use search_results::SearchResult;
+use serde::{Deserialize, Serialize};
+use walkdir::WalkDir;
+
+use std::io::{self, BufRead};
+
+#[derive(Default, Serialize, Deserialize)]
+pub struct Search {
+ search_type: SearchType,
+ file_names: BTreeSet<String>,
+ file_contents: BTreeMap<(String, usize), String>, // file_name, line_number, line
+ cached_file_name_results: HashMap<String, Vec<SearchResult>>,
+ cached_file_contents_results: HashMap<String, Vec<SearchResult>>,
+}
+
+impl Search {
+ pub fn new(search_type: SearchType) -> Self {
+ Search {
+ search_type,
+ ..Default::default()
+ }
+ }
+ fn on_message(&mut self, message: String, payload: String) {
+ match serde_json::from_str::<MessageToSearch>(&message) {
+ Ok(MessageToSearch::ScanFolder) => {
+ self.scan_hd();
+ post_message_to_plugin(
+ serde_json::to_string(&MessageToPlugin::DoneScanningFolder).unwrap(),
+ "".to_owned(),
+ );
+ },
+ Ok(MessageToSearch::Search) => {
+ if let Some(current_search_term) = self.read_search_term_from_hd_cache() {
+ self.search(current_search_term);
+ }
+ },
+ Ok(MessageToSearch::FileSystemCreate) => {
+ self.rescan_files(payload);
+ },
+ Ok(MessageToSearch::FileSystemUpdate) => {
+ self.rescan_files(payload);
+ },
+ Ok(MessageToSearch::FileSystemDelete) => {
+ self.delete_files(payload);
+ },
+ Err(e) => eprintln!("Failed to deserialize worker message {:?}", e),
+ }
+ }
+ pub fn scan_hd(&mut self) {
+ for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) {
+ self.add_file_entry(entry.path(), entry.metadata().ok());
+ }
+ }
+ pub fn search(&mut self, search_term: String) {
+ let search_results_limit = 100; // artificial limit to prevent probably unwanted chaos
+ // let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(search_results_limit);
+ let mut file_names_search_results = None;
+ let mut file_contents_search_results = None;
+ if let SearchType::Names | SearchType::NamesAndContents = self.search_type {
+ let file_names_matches = match self.cached_file_name_results.get(&search_term) {
+ Some(cached_results) => cached_results.clone(),
+ None => {
+ let mut matcher = SkimMatcherV2::default().use_cache(true);
+ let results = self.search_file_names(&search_term, &mut matcher);
+ self.cached_file_name_results
+ .insert(search_term.clone(), results.clone());
+ results
+ },
+ };
+ file_names_search_results = Some(
+ ResultsOfSearch::new(search_term.clone(), file_names_matches)
+ .limit_search_results(search_results_limit),
+ );
+ };
+ if let SearchType::Contents | SearchType::NamesAndContents = self.search_type {
+ let file_contents_matches = match self.cached_file_contents_results.get(&search_term) {
+ Some(cached_results) => cached_results.clone(),
+ None => {
+ let mut matcher = SkimMatcherV2::default().use_cache(true);
+ let results = self.search_file_contents(&search_term, &mut matcher);
+ self.cached_file_contents_results
+ .insert(search_term.clone(), results.clone());
+ results
+ },
+ };
+ file_contents_search_results = Some(
+ ResultsOfSearch::new(search_term.clone(), file_contents_matches)
+ .limit_search_results(search_results_limit),
+ );
+ };
+
+ // if the search term changed before we finished, let's search again!
+ if let Some(current_search_term) = self.read_search_term_from_hd_cache() {
+ if current_search_term != search_term {
+ return self.search(current_search_term.into());
+ }
+ }
+ if let Some(file_names_search_results) = file_names_search_results {
+ post_message_to_plugin(
+ serde_json::to_string(&MessageToPlugin::UpdateFileNameSearchResults).unwrap(),
+ serde_json::to_string(&file_names_search_results).unwrap(),
+ );
+ }
+ if let Some(file_contents_search_results) = file_contents_search_results {
+ post_message_to_plugin(
+ serde_json::to_string(&MessageToPlugin::UpdateFileContentsSearchResults).unwrap(),
+ serde_json::to_string(&file_contents_search_results).unwrap(),
+ );
+ }
+ }
+ pub fn rescan_files(&mut self, paths: String) {
+ match serde_json::from_str::<Vec<PathBuf>>(&paths) {
+ Ok(paths) => {
+ for path in paths {
+ self.add_file_entry(&path, path.metadata().ok());
+ }
+ self.cached_file_name_results.clear();
+ self.cached_file_contents_results.clear();
+ },
+ Err(e) => eprintln!("Failed to deserialize paths: {:?}", e),
+ }
+ }
+ pub fn delete_files(&mut self, paths: String) {
+ match serde_json::from_str::<Vec<PathBuf>>(&paths) {
+ Ok(paths) => {
+ self.remove_existing_entries(&paths);
+ self.cached_file_name_results.clear();
+ self.cached_file_contents_results.clear();
+ },
+ Err(e) => eprintln!("Failed to deserialize paths: {:?}", e),
+ }
+ }
+ fn add_file_entry(&mut self, file_name: &Path, file_metadata: Option<std::fs::Metadata>) {
+ let file_path = file_name.display().to_string();
+ let file_path_stripped_prefix = self.strip_file_prefix(&file_name);
+
+ self.file_names.insert(file_path_stripped_prefix.clone());
+ if let SearchType::NamesAndContents | SearchType::Contents = self.search_type {
+ if file_metadata.map(|f| f.is_file()).unwrap_or(false) {
+ 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.file_contents.insert(
+ (
+ // String::from_utf8_lossy(&strip_ansi_escapes::strip(file_path_stripped_prefix.clone()).unwrap()).to_string(),
+ file_path_stripped_prefix.clone(),
+ index + 1,
+ ),
+ String::from_utf8_lossy(
+ &strip_ansi_escapes::strip(line).unwrap(),
+ )
+ .to_string(),
+ );
+ },
+ Err(_) => {
+ break; // probably a binary file, skip it
+ },
+ }
+ }
+ }
+ }
+ }
+ }
+ fn search_file_names(
+ &self,
+ search_term: &str,
+ matcher: &mut SkimMatcherV2,
+ ) -> Vec<SearchResult> {
+ let mut matches = vec![];
+ for entry in &self.file_names {
+ if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) {
+ matches.push(SearchResult::new_file_name(
+ score,
+ indices,
+ entry.to_owned(),
+ ));
+ }
+ }
+ matches
+ }
+ fn search_file_contents(
+ &self,
+ search_term: &str,
+ matcher: &mut SkimMatcherV2,
+ ) -> Vec<SearchResult> {
+ let mut matches = vec![];
+ for ((file_name, line_number), line_entry) in &self.file_contents {
+ if line_entry.contains("struct") {
+ if line_entry.len() < 400 {
+ eprintln!("matching against: {:?}", line_entry)
+ } else {
+ eprintln!("matching again line that has struct but is very long")
+ }
+ }
+ if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) {
+ if line_entry.contains("struct") {
+ eprintln!("score: {:?}", score)
+ }
+ matches.push(SearchResult::new_file_line(
+ score,
+ indices,
+ file_name.clone(),
+ line_entry.clone(),
+ *line_number,
+ ));
+ } else {
+ if line_entry.contains("struct") {
+ eprintln!("no score!")
+ }
+ }
+ }
+ matches
+ }
+ fn strip_file_prefix(&self, file_name: &Path) -> String {
+ let mut file_path_stripped_prefix = file_name.display().to_string().split_off(ROOT.width());
+ if file_path_stripped_prefix.starts_with('/') {
+ file_path_stripped_prefix.remove(0);
+ }
+ file_path_stripped_prefix
+ }
+ fn read_search_term_from_hd_cache(&self) -> Option<String> {
+ match std::fs::read(CURRENT_SEARCH_TERM) {
+ Ok(current_search_term) => {
+ Some(String::from_utf8_lossy(&current_search_term).to_string())
+ },
+ _ => None,
+ }
+ }
+ fn remove_existing_entries(&mut self, paths: &Vec<PathBuf>) {
+ let file_path_stripped_prefixes: Vec<String> =
+ paths.iter().map(|p| self.strip_file_prefix(&p)).collect();
+ self.file_names
+ .retain(|file_name| !file_path_stripped_prefixes.contains(file_name));
+ self.file_contents.retain(|(file_name, _line_in_file), _| {
+ !file_path_stripped_prefixes.contains(file_name)
+ });
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum MessageToSearch {
+ ScanFolder,
+ Search,
+ FileSystemCreate,
+ FileSystemUpdate,
+ FileSystemDelete,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FileNameWorker {
+ search: Search,
+}
+
+impl Default for FileNameWorker {
+ fn default() -> Self {
+ FileNameWorker {
+ search: Search::new(SearchType::Names),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FileContentsWorker {
+ search: Search,
+}
+
+impl Default for FileContentsWorker {
+ fn default() -> Self {
+ FileContentsWorker {
+ search: Search::new(SearchType::Contents),
+ }
+ }
+}
+
+impl<'de> ZellijWorker<'de> for FileNameWorker {
+ fn on_message(&mut self, message: String, payload: String) {
+ self.search.on_message(message, payload);
+ }
+}
+
+impl<'de> ZellijWorker<'de> for FileContentsWorker {
+ fn on_message(&mut self, message: String, payload: String) {
+ self.search.on_message(message, payload);
+ }
+}
+
+#[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
+ }
+}
diff --git a/default-plugins/strider/src/search/search_results.rs b/default-plugins/strider/src/search/search_results.rs
new file mode 100644
index 00000000..b76200ae
--- /dev/null
+++ b/default-plugins/strider/src/search/search_results.rs
@@ -0,0 +1,308 @@
+use crate::search::ui::{
+ bold, styled_text, styled_text_background, styled_text_foreground, underline, GRAY_LIGHT,
+ GREEN, ORANGE,
+};
+use serde::{Deserialize, Serialize};
+use unicode_width::UnicodeWidthStr;
+
+#[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 is_same_entry(&self, other: &Self) -> bool {
+ match (&self, other) {
+ (
+ SearchResult::File { path: my_path, .. },
+ SearchResult::File {
+ path: other_path, ..
+ },
+ ) => my_path == other_path,
+ (
+ SearchResult::LineInFile {
+ path: my_path,
+ line_number: my_line_number,
+ ..
+ },
+ SearchResult::LineInFile {
+ path: other_path,
+ line_number: other_line_number,
+ ..
+ },
+ ) => my_path == other_path && my_line_number == other_line_number,
+ _ => false,
+ }
+ }
+ pub fn render(
+ &self,
+ max_width: usize,
+ is_selected: bool,
+ is_below_search_result: bool,
+ ) -> String {
+ let max_width = max_width.saturating_sub(4); // for the UI left line separator
+ match self {
+ SearchResult::File { path, indices, .. } => self.render_file_result(
+ path,
+ indices,
+ is_selected,
+ is_below_search_result,
+ max_width,
+ ),
+ SearchResult::LineInFile {
+ path,
+ line,
+ line_number,
+ indices,
+ ..
+ } => self.render_line_in_file_result(
+ path,
+ line,
+ *line_number,
+ indices,
+ is_selected,
+ is_below_search_result,
+ max_width,
+ ),
+ }
+ }
+ fn render_file_result(
+ &self,
+ path: &String,
+ indices: &Vec<usize>,
+ is_selected: bool,
+ is_below_search_result: bool,
+ max_width: usize,
+ ) -> String {
+ if is_selected {
+ let line = self.render_line_with_indices(
+ path,
+ indices,
+ max_width.saturating_sub(3),
+ Some(GREEN),
+ );
+ let selection_arrow = styled_text_foreground(ORANGE, "┌>");
+ format!("{} {}", selection_arrow, line)
+ } else {
+ let line_prefix = if is_below_search_result { "│ " } else { " " };
+ let line =
+ self.render_line_with_indices(path, indices, max_width.saturating_sub(3), None);
+ format!("{} {}", line_prefix, line)
+ }
+ }
+ fn render_line_in_file_result(
+ &self,
+ path: &String,
+ line: &String,
+ line_number: usize,
+ indices: &Vec<usize>,
+ is_selected: bool,
+ is_below_search_result: bool,
+ max_width: usize,
+ ) -> String {
+ let line_number_prefix_text = format!("└ {} ", line_number);
+ let max_width_of_line_in_file = max_width
+ .saturating_sub(3)
+ .saturating_sub(line_number_prefix_text.width());
+ if is_selected {
+ let file_name_line = self.render_line_with_indices(
+ path,
+ &vec![],
+ max_width.saturating_sub(3),
+ Some(GREEN),
+ );
+ let line_in_file = self.render_line_with_indices(
+ line,
+ indices,
+ max_width_of_line_in_file,
+ Some(GREEN),
+ );
+ let line_number_prefix = styled_text_foreground(GREEN, &bold(&line_number_prefix_text));
+ format!(
+ "{} {}\n│ {}{}",
+ styled_text_foreground(ORANGE, "┌>"),
+ file_name_line,
+ line_number_prefix,
+ line_in_file
+ )
+ } else {
+ let file_name_line =
+ self.render_line_with_indices(path, &vec![], max_width.saturating_sub(3), None);
+ let line_in_file =
+ self.render_line_with_indices(line, indices, max_width_of_line_in_file, None);
+ let line_number_prefix = bold(&line_number_prefix_text);
+ let line_prefix = if is_below_search_result { "│ " } else { " " };
+ format!(
+ "{} {}\n{} {}{}",
+ line_prefix, file_name_line, line_prefix, line_number_prefix, line_in_file
+ )
+ }
+ }
+ fn render_line_with_indices(
+ &self,
+ line_to_render: &String,
+ indices: &Vec<usize>,
+ max_width: usize,
+ foreground_color: Option<u8>,
+ ) -> String {
+ let non_index_character_style = |c: &str| match foreground_color {
+ Some(foreground_color) => styled_text_foreground(foreground_color, &bold(c)),
+ None => bold(c),
+ };
+ let index_character_style = |c: &str| match foreground_color {
+ Some(foreground_color) => {
+ styled_text(foreground_color, GRAY_LIGHT, &bold(&underline(c)))
+ },
+ None => styled_text_background(GRAY_LIGHT, &bold(&underline(c))),
+ };
+
+ let truncate_positions =
+ self.truncate_line_with_indices(line_to_render, indices, max_width);
+ let truncate_start_position = truncate_positions.map(|p| p.0).unwrap_or(0);
+ let truncate_end_position = truncate_positions
+ .map(|p| p.1)
+ .unwrap_or(line_to_render.chars().count());
+ let mut visible_portion = String::new();
+ for (i, character) in line_to_render.chars().enumerate() {
+ if i >= truncate_start_position && i <= truncate_end_position {
+ if indices.contains(&i) {
+ visible_portion.push_str(&index_character_style(&character.to_string()));
+ } else {
+ visible_portion.push_str(&non_index_character_style(&character.to_string()));
+ }
+ }
+ }
+ if truncate_positions.is_some() {
+ let left_truncate_sign = if truncate_start_position == 0 {
+ ""
+ } else {
+ ".."
+ };
+ let right_truncate_sign = if truncate_end_position == line_to_render.chars().count() {
+ ""
+ } else {
+ ".."
+ };
+ format!(
+ "{}{}{}",
+ non_index_character_style(left_truncate_sign),
+ visible_portion,
+ non_index_character_style(right_truncate_sign)
+ )
+ } else {
+ visible_portion
+ }
+ }
+ fn truncate_line_with_indices(
+ &self,
+ line_to_render: &String,
+ indices: &Vec<usize>,
+ max_width: usize,
+ ) -> Option<(usize, usize)> {
+ let first_index = indices.get(0).copied().unwrap_or(0);
+ let last_index = indices
+ .last()
+ .copied()
+ .unwrap_or_else(|| std::cmp::min(line_to_render.chars().count(), max_width));
+ if line_to_render.width() <= max_width {
+ // there's enough room, no need to truncate
+ None
+ } else if last_index.saturating_sub(first_index) < max_width {
+ // truncate around the indices
+ let mut width_remaining = max_width
+ .saturating_sub(1)
+ .saturating_sub(last_index.saturating_sub(first_index));
+
+ let mut string_start_position = first_index;
+ let mut string_end_position = last_index;
+
+ let mut i = 0;
+ loop {
+ if i >= width_remaining {
+ break;
+ }
+ if string_start_position > 0 && string_end_position < line_to_render.chars().count()
+ {
+ let take_from_start = i % 2 == 0;
+ if take_from_start {
+ string_start_position -= 1;
+ if string_start_position == 0 {
+ width_remaining += 2; // no need for truncating dots
+ }
+ } else {
+ string_end_position += 1;
+ if string_end_position == line_to_render.chars().count() {
+ width_remaining += 2; // no need for truncating dots
+ }
+ }
+ } else if string_end_position < line_to_render.chars().count() {
+ string_end_position += 1;
+ if string_end_position == line_to_render.chars().count() {
+ width_remaining += 2; // no need for truncating dots
+ }
+ } else if string_start_position > 0 {
+ string_start_position -= 1;
+ if string_start_position == 0 {
+ width_remaining += 2; // no need for truncating dots
+ }
+ } else {
+ break;
+ }
+ i += 1;
+ }
+ Some((string_start_position, string_end_position))
+ } else if !indices.is_empty() {
+ // no room for all indices, remove the last one and try again
+ let mut new_indices = indices.clone();
+ new_indices.pop();
+ self.truncate_line_with_indices(line_to_render, &new_indices, max_width)
+ } else {
+ Some((first_index, last_index))
+ }
+ }
+}
diff --git a/default-plugins/strider/src/search/search_state.rs b/default-plugins/strider/src/search/search_state.rs
new file mode 100644
index 00000000..5766d1c2
--- /dev/null
+++ b/default-plugins/strider/src/search/search_state.rs
@@ -0,0 +1,241 @@
+use crate::search::search_results::SearchResult;
+use crate::search::{MessageToSearch, ResultsOfSearch};
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use zellij_tile::prelude::{
+ hide_self, open_file, open_file_floating, open_file_with_line, open_file_with_line_floating,
+ open_terminal, open_terminal_floating, post_message_to, Key,
+};
+
+pub const CURRENT_SEARCH_TERM: &str = "/data/current_search_term";
+
+#[derive(Default)]
+pub struct SearchState {
+ pub search_term: String,
+ pub file_name_search_results: Vec<SearchResult>,
+ pub file_contents_search_results: Vec<SearchResult>,
+ pub loading: bool,
+ pub loading_animation_offset: u8,
+ pub selected_search_result: usize,
+ pub should_open_floating: bool,
+ pub search_filter: SearchType,
+ pub display_rows: usize,
+ pub display_columns: usize,
+ pub displayed_search_results: (usize, Vec<SearchResult>), // usize is selected index
+}
+
+impl SearchState {
+ pub fn handle_key(&mut self, key: Key) {
+ match key {
+ Key::Down => self.move_search_selection_down(),
+ Key::Up => self.move_search_selection_up(),
+ Key::Char('\n') => self.open_search_result_in_editor(),
+ Key::BackTab => self.open_search_result_in_terminal(),
+ Key::Ctrl('f') => {
+ self.should_open_floating = !self.should_open_floating;
+ },
+ Key::Ctrl('r') => self.toggle_search_filter(),
+ Key::Esc => {
+ hide_self();
+ self.clear_state();
+ },
+ _ => self.append_to_search_term(key),
+ }
+ }
+ pub fn update_file_name_search_results(&mut self, mut results_of_search: ResultsOfSearch) {
+ if self.search_term == results_of_search.search_term {
+ self.file_name_search_results = results_of_search.search_results.drain(..).collect();
+ self.update_displayed_search_results();
+ }
+ }
+ pub fn update_file_contents_search_results(&mut self, mut results_of_search: ResultsOfSearch) {
+ if self.search_term == results_of_search.search_term {
+ self.file_contents_search_results =
+ results_of_search.search_results.drain(..).collect();
+ self.update_displayed_search_results();
+ }
+ }
+ pub fn change_size(&mut self, rows: usize, cols: usize) {
+ self.display_rows = rows;
+ self.display_columns = cols;
+ }
+ pub fn progress_animation(&mut self) {
+ if self.loading_animation_offset == u8::MAX {
+ self.loading_animation_offset = 0;
+ } else {
+ self.loading_animation_offset = self.loading_animation_offset.saturating_add(1);
+ }
+ }
+ pub fn number_of_lines_in_displayed_search_results(&self) -> usize {
+ self.displayed_search_results
+ .1
+ .iter()
+ .map(|l| l.rendered_height())
+ .sum()
+ }
+ fn move_search_selection_down(&mut self) {
+ if self.displayed_search_results.0 < self.max_search_selection_index() {
+ self.displayed_search_results.0 += 1;
+ }
+ }
+ fn move_search_selection_up(&mut self) {
+ self.displayed_search_results.0 = self.displayed_search_results.0.saturating_sub(1);
+ }
+ fn open_search_result_in_editor(&mut self) {
+ match self.selected_search_result_entry() {
+ Some(SearchResult::File { path, .. }) => {
+ if self.should_open_floating {
+ open_file_floating(&PathBuf::from(path))
+ } else {
+ open_file(&PathBuf::from(path));
+ }
+ },
+ Some(SearchResult::LineInFile {
+ path, line_number, ..
+ }) => {
+ if self.should_open_floating {
+ open_file_with_line_floating(&PathBuf::from(path), line_number);
+ } else {
+ open_file_with_line(&PathBuf::from(path), line_number);
+ }
+ },
+ None => eprintln!("Search results not found"),
+ }
+ }
+ fn open_search_result_in_terminal(&mut self) {
+ let dir_path_of_result = |path: &str| -> PathBuf {
+ let file_path = PathBuf::from(path);
+ let mut dir_path = file_path.components();
+ dir_path.next_back(); // remove file name to stay with just the folder
+ dir_path.as_path().into()
+ };
+ let selected_search_result_entry = self.selected_search_result_entry();
+ if let Some(SearchResult::File { path, .. }) | Some(SearchResult::LineInFile { path, .. }) =
+ selected_search_result_entry
+ {
+ let dir_path = dir_path_of_result(&path);
+ if self.should_open_floating {
+ open_terminal_floating(&dir_path);
+ } else {
+ open_terminal(&dir_path);
+ }
+ }
+ }
+ fn toggle_search_filter(&mut self) {
+ self.search_filter.progress();
+ self.send_search_query();
+ }
+ fn clear_state(&mut self) {
+ self.file_name_search_results.clear();
+ self.file_contents_search_results.clear();
+ self.displayed_search_results = (0, vec![]);
+ self.search_term.clear();
+ }
+ fn append_to_search_term(&mut self, key: Key) {
+ match key {
+ Key::Char(character) => {
+ self.search_term.push(character);
+ },
+ Key::Backspace => {
+ self.search_term.pop();
+ if self.search_term.len() == 0 {
+ self.clear_state();
+ }
+ },
+ _ => {},
+ }
+ self.send_search_query();
+ }
+ fn send_search_query(&mut self) {
+ match std::fs::write(CURRENT_SEARCH_TERM, &self.search_term) {
+ Ok(_) => {
+ if !self.search_term.is_empty() {
+ post_message_to(
+ "file_name_search",
+ serde_json::to_string(&MessageToSearch::Search).unwrap(),
+ "".to_owned(),
+ );
+ post_message_to(
+ "file_contents_search",
+ serde_json::to_string(&MessageToSearch::Search).unwrap(),
+ "".to_owned(),
+ );
+ self.file_name_search_results.clear();
+ self.file_contents_search_results.clear();
+ }
+ },
+ Err(e) => eprintln!("Failed to write search term to HD, aborting search: {}", e),
+ }
+ }
+ fn max_search_selection_index(&self) -> usize {
+ self.displayed_search_results.1.len().saturating_sub(1)
+ }
+ fn update_displayed_search_results(&mut self) {
+ if self.search_term.is_empty() {
+ self.clear_state();
+ return;
+ }
+ let mut search_results_of_interest = match self.search_filter {
+ SearchType::NamesAndContents => {
+ let mut all_search_results = self.file_name_search_results.clone();
+ all_search_results.append(&mut self.file_contents_search_results.clone());
+ all_search_results.sort_by(|a, b| b.score().cmp(&a.score()));
+ all_search_results
+ },
+ SearchType::Names => self.file_name_search_results.clone(),
+ SearchType::Contents => self.file_contents_search_results.clone(),
+ };
+ let mut height_taken_up_by_results = 0;
+ let mut displayed_search_results = vec![];
+ for search_result in search_results_of_interest.drain(..) {
+ if height_taken_up_by_results + search_result.rendered_height()
+ > self.rows_for_results()
+ {
+ break;
+ }
+ height_taken_up_by_results += search_result.rendered_height();
+ displayed_search_results.push(search_result);
+ }
+ let new_index = self
+ .selected_search_result_entry()
+ .and_then(|currently_selected_search_result| {
+ displayed_search_results
+ .iter()
+ .position(|r| r.is_same_entry(&currently_selected_search_result))
+ })
+ .unwrap_or(0);
+ self.displayed_search_results = (new_index, displayed_search_results);
+ }
+ fn selected_search_result_entry(&self) -> Option<SearchResult> {
+ self.displayed_search_results
+ .1
+ .get(self.displayed_search_results.0)
+ .cloned()
+ }
+ pub fn rows_for_results(&self) -> usize {
+ self.display_rows.saturating_sub(3) // search line and 2 controls lines
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum SearchType {
+ NamesAndContents,
+ Names,
+ Contents,
+}
+
+impl SearchType {
+ pub fn progress(&mut self) {
+ match &self {
+ &SearchType::NamesAndContents => *self = SearchType::Names,
+ &SearchType::Names => *self = SearchType::Contents,
+ &SearchType::Contents => *self = SearchType::NamesAndContents,
+ }
+ }
+}
+
+impl Default for SearchType {
+ fn default() -> Self {
+ SearchType::NamesAndContents
+ }
+}
diff --git a/default-plugins/strider/src/search/selection_controls_area.rs b/default-plugins/strider/src/search/selection_controls_area.rs
new file mode 100644
index 00000000..0805e1e4
--- /dev/null
+++ b/default-plugins/strider/src/search/selection_controls_area.rs
@@ -0,0 +1,61 @@
+use crate::search::ui::{bold, styled_text_foreground, ORANGE};
+
+pub struct SelectionControlsArea {
+ display_lines: usize,
+ display_columns: usize,
+}
+
+impl SelectionControlsArea {
+ pub fn new(display_lines: usize, display_columns: usize) -> Self {
+ SelectionControlsArea {
+ display_lines,
+ display_columns,
+ }
+ }
+ pub fn render(&self, result_count: usize) -> String {
+ let mut to_render = String::new();
+ let padding = self.display_lines.saturating_sub(result_count);
+ for _ in 0..padding {
+ to_render.push_str(&self.render_padding_line());
+ }
+ let selection_controls = self.render_selection_controls();
+ to_render.push_str(&selection_controls);
+ to_render
+ }
+ pub fn render_empty_lines(&self) -> String {
+ let mut to_render = String::new();
+ for _ in 0..self.display_lines {
+ to_render.push_str("\n");
+ }
+ to_render
+ }
+ fn render_padding_line(&self) -> String {
+ format!("│\n")
+ }
+ fn render_selection_controls(&self) -> String {
+ if self.display_columns >= self.full_selection_controls_len() {
+ self.render_full_selection_controls()
+ } else {
+ self.render_truncated_selection_controls()
+ }
+ }
+ fn full_selection_controls_len(&self) -> usize {
+ 62
+ }
+ fn render_full_selection_controls(&self) -> String {
+ let arrow_tail = "└ ";
+ let enter = styled_text_foreground(ORANGE, &bold("<ENTER>"));
+ let enter_tip = bold(" - open in editor. ");
+ let tab = styled_text_foreground(ORANGE, &bold("<TAB>"));
+ let tab_tip = bold(" - open terminal at location.");
+ format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip)
+ }
+ fn render_truncated_selection_controls(&self) -> String {
+ let arrow_tail = "└ ";
+ let enter = styled_text_foreground(ORANGE, &bold("<ENTER>"));
+ let enter_tip = bold(" - edit. ");
+ let tab = styled_text_foreground(ORANGE, &bold("<TAB>"));
+ let tab_tip = bold(" - terminal.");
+ format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip)
+ }
+}
diff --git a/default-plugins/strider/src/search/ui.rs b/default-plugins/strider/src/search/ui.rs
new file mode 100644
index 00000000..5ee5a3e8
--- /dev/null
+++ b/default-plugins/strider/src/search/ui.rs
@@ -0,0 +1,120 @@
+use crate::search::controls_line::{Control, ControlsLine};
+use crate::search::search_state::SearchState;
+use crate::search::selection_controls_area::SelectionControlsArea;
+use std::fmt::{Display, Formatter, Result};
+
+pub const CYAN: u8 = 51;
+pub const GRAY_LIGHT: u8 = 238;
+pub const GRAY_DARK: u8 = 245;
+pub const WHITE: u8 = 15;
+pub const BLACK: u8 = 16;
+pub const RED: u8 = 124;
+pub const GREEN: u8 = 154;
+pub const ORANGE: u8 = 166;
+
+impl Display for SearchState {
+ fn fmt(&self, f: &mut Formatter) -> Result {
+ write!(f, "{}", self.render_search_line())?;
+ write!(f, "{}", self.render_search_results())?;
+ write!(f, "{}", self.render_selection_control_area())?;
+ write!(f, "{}", self.render_controls_line())?;
+ Ok(())
+ }
+}
+
+impl SearchState {
+ pub fn render_search_line(&self) -> String {
+ format!(
+ "{}{}\n",
+ styled_text_foreground(CYAN, &bold("SEARCH: ")),
+ self.search_term
+ )
+ }
+ pub fn render_search_results(&self) -> String {
+ let mut space_for_results = self.display_rows.saturating_sub(3); // title and both controls lines
+ let mut to_render = String::new();
+ for (i, search_result) in self.displayed_search_results.1.iter().enumerate() {
+ let result_height = search_result.rendered_height();
+ if space_for_results < result_height {
+ break;
+ }
+ space_for_results -= result_height;
+ let index_of_selected_result = self.displayed_search_results.0;
+ let is_selected = i == index_of_selected_result;
+ let is_below_search_result = i > index_of_selected_result;
+ let rendered_result =
+ search_result.render(self.display_columns, is_selected, is_below_search_result);
+ to_render.push_str(&format!("{}", rendered_result));
+ to_render.push('\n')
+ }
+ to_render
+ }
+ pub fn render_selection_control_area(&self) -> String {
+ let rows_for_results = self.rows_for_results();
+ if !self.displayed_search_results.1.is_empty() {
+ format!(
+ "{}\n",
+ SelectionControlsArea::new(rows_for_results, self.display_columns)
+ .render(self.number_of_lines_in_displayed_search_results())
+ )
+ } else {
+ format!(
+ "{}\n",
+ SelectionControlsArea::new(rows_for_results, self.display_columns)
+ .render_empty_lines()
+ )
+ }
+ }
+ pub fn render_controls_line(&self) -> String {
+ let has_results = !self.displayed_search_results.1.is_empty();
+ let tiled_floating_control =
+ Control::new_floating_control("Ctrl f", self.should_open_floating);
+ let names_contents_control = Control::new_filter_control("Ctrl r", &self.search_filter);
+ if self.loading {
+ ControlsLine::new(
+ vec![tiled_floating_control, names_contents_control],
+ Some(vec!["Scanning folder", "Scanning", "S"]),
+ )
+ .with_animation_offset(self.loading_animation_offset)
+ .render(self.display_columns, has_results)
+ } else {
+ ControlsLine::new(vec![tiled_floating_control, names_contents_control], None)
+ .render(self.display_columns, has_results)
+ }
+ }
+}
+
+pub fn bold(text: &str) -> String {
+ format!("\u{1b}[1m{}\u{1b}[m", text)
+}
+
+pub fn underline(text: &str) -> String {
+ format!("\u{1b}[4m{}\u{1b}[m", text)
+}
+
+pub fn styled_text(foreground_color: u8, background_color: u8, text: &str) -> String {
+ format!(
+ "\u{1b}[38;5;{};48;5;{}m{}\u{1b}[m",
+ foreground_color, background_color, text
+ )
+}
+
+pub fn styled_text_foreground(foreground_color: u8, text: &str) -> String {
+ format!("\u{1b}[38;5;{}m{}\u{1b}[m", foreground_color, text)
+}
+
+pub fn styled_text_background(background_color: u8, text: &str) -> String {
+ format!("\u{1b}[48;5;{}m{}\u{1b}[m", background_color, text)
+}
+
+pub fn color_line_to_end(background_color: u8) -> String {
+ format!("\u{1b}[48;5;{}m\u{1b}[0K", background_color)
+}
+
+pub fn arrow(foreground: u8, background: u8) -> String {
+ format!("\u{1b}[38;5;{}m\u{1b}[48;5;{}m", foreground, background)
+}
+
+pub fn dot(foreground: u8, background: u8) -> String {
+ format!("\u{1b}[38;5;{};48;5;{}m•", foreground, background)
+}
diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs
index 7ced4932..8dd09653 100644
--- a/default-plugins/strider/src/state.rs
+++ b/default-plugins/strider/src/state.rs
@@ -1,4 +1,6 @@
-use crate::search::SearchResult;
+use crate::search::search_results::SearchResult;
+use crate::search::search_state::SearchState;
+use crate::search::search_state::SearchType;
use pretty_bytes::converter as pb;
use std::{
collections::{HashMap, VecDeque},
@@ -18,65 +20,29 @@ pub struct State {
pub cursor_hist: HashMap<PathBuf, (usize, usize)>,
pub hide_hidden_files: bool,
pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future
+ pub search_state: SearchState,
pub search_paths: Vec<String>,
pub search_term: Option<String>,
- pub search_results: Vec<SearchResult>,
+ pub file_name_search_results: Vec<SearchResult>,
+ pub file_contents_search_results: Vec<SearchResult>,
pub loading: bool,
pub loading_animation_offset: u8,
pub typing_search_term: bool,
- pub exploring_search_results: bool,
pub selected_search_result: usize,
+ pub processed_search_index: usize,
+ pub should_open_floating: bool,
+ pub search_filter: SearchType,
}
impl State {
- pub fn append_to_search_term(&mut self, key: Key) {
- match key {
- Key::Char(character) => {
- if let Some(search_term) = self.search_term.as_mut() {
- search_term.push(character);
- }
- },
- Key::Backspace => {
- if let Some(search_term) = self.search_term.as_mut() {
- search_term.pop();
- if search_term.len() == 0 {
- self.search_term = None;
- self.typing_search_term = false;
- }
- }
- },
- _ => {},
- }
- }
- pub fn accept_search_term(&mut self) {
- self.typing_search_term = false;
- self.exploring_search_results = true;
- }
pub fn typing_search_term(&self) -> bool {
self.typing_search_term
}
- pub fn exploring_search_results(&self) -> bool {
- self.exploring_search_results
- }
- pub fn stop_exploring_search_results(&mut self) {
- self.exploring_search_results = false;
- }
pub fn start_typing_search_term(&mut self) {
- if self.search_term.is_none() {
- self.search_term = Some(String::new());
- }
self.typing_search_term = true;
}
pub fn stop_typing_search_term(&mut self) {
- self.typing_search_term = true;
- }
- pub fn move_search_selection_up(&mut self) {
- self.selected_search_result = self.selected_search_result.saturating_sub(1);
- }
- pub fn move_search_selection_down(&mut self) {
- if self.selected_search_result < self.search_results.len() {
- self.selected_search_result = self.selected_search_result.saturating_add(1);
- }
+ self.typing_search_term = false;
}
pub fn selected_mut(&mut self) -> &mut usize {
&mut self.cursor_hist.entry(self.path.clone()).or_default().0
@@ -104,32 +70,6 @@ impl State {
}
}
}
- pub fn open_search_result(&mut self) {
- match self.search_results.get(self.selected_search_result) {
- Some(SearchResult::File {
- path,
- score,
- indices,
- }) => {
- let file_path = PathBuf::from(path);
- open_file(file_path.strip_prefix(ROOT).unwrap());
- },
- Some(SearchResult::LineInFile {
- path,
- score,
- indices,
- line,
- line_number,
- }) => {
- let file_path = PathBuf::from(path);
- open_file_with_line(file_path.strip_prefix(ROOT).unwrap(), *line_number);
- // open_file_with_line(&file_path, *line_number); // TODO: no!!
- },
- None => {
- eprintln!("Search result not found");
- },
- }
- }
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]