summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Oram <dev@mitmaro.ca>2023-03-06 09:15:19 -0330
committerTim Oram <dev@mitmaro.ca>2023-09-09 20:11:35 -0230
commit8192277e48ae83e729d79b235602e0e0b318cc30 (patch)
tree4f584428739bad06c3242450b7d367dc409185e0
parentd7655157ff5d8e199291575b2ac0d1ac6267d69f (diff)
-rw-r--r--Cargo.lock96
-rw-r--r--Makefile.toml7
-rw-r--r--src/core/src/components/search_bar/mod.rs4
-rw-r--r--src/core/src/components/search_bar/options.rs15
-rw-r--r--src/core/src/components/search_bar/tests.rs11
-rw-r--r--src/core/src/modules/list/mod.rs165
-rw-r--r--src/core/src/modules/list/search/line_match.rs24
-rw-r--r--src/core/src/modules/list/search/mod.rs6
-rw-r--r--src/core/src/modules/list/search/search.rs750
-rw-r--r--src/core/src/modules/list/search/state.rs235
-rw-r--r--src/core/src/modules/list/tests/activate.rs65
-rw-r--r--src/core/src/modules/list/tests/external_editor.rs20
-rw-r--r--src/core/src/modules/list/tests/mod.rs1
-rw-r--r--src/core/src/modules/list/tests/old_search.xrs599
-rw-r--r--src/core/src/modules/list/tests/search.rs617
-rw-r--r--src/core/src/modules/list/utils.rs47
-rw-r--r--src/core/src/process/artifact.rs2
-rw-r--r--src/core/src/search/mod.rs2
-rw-r--r--src/core/src/search/testutil.rs27
-rw-r--r--src/core/src/testutil/action_line.rs2
-rw-r--r--src/core/src/testutil/assert_results.rs100
-rw-r--r--src/core/src/testutil/mod.rs4
-rw-r--r--src/core/src/util.rs1
23 files changed, 2257 insertions, 543 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d7064e1..1a5d975 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "aho-corasick"
-version = "1.0.2"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
dependencies = [
"memchr",
]
@@ -28,9 +28,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.72"
+version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "arrayvec"
@@ -52,9 +52,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "bumpalo"
@@ -357,7 +357,7 @@ name = "girt-core"
version = "2.3.0"
dependencies = [
"anyhow",
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"captur",
"chrono",
"claims",
@@ -417,7 +417,7 @@ name = "girt-input"
version = "2.3.0"
dependencies = [
"anyhow",
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"captur",
"crossbeam-channel",
"crossterm",
@@ -468,7 +468,7 @@ name = "girt-view"
version = "2.3.0"
dependencies = [
"anyhow",
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"captur",
"claims",
"crossbeam-channel",
@@ -652,9 +652,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.19"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
@@ -736,9 +736,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project-lite"
-version = "0.2.11"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c"
+checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
[[package]]
name = "pin-utils"
@@ -779,9 +779,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.32"
+version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
@@ -856,15 +856,15 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "relative-path"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698"
+checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca"
[[package]]
name = "rstest"
-version = "0.18.1"
+version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b96577ca10cb3eade7b337eb46520108a67ca2818a24d0b63f41fd62bc9651c"
+checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199"
dependencies = [
"futures",
"futures-timer",
@@ -874,9 +874,9 @@ dependencies = [
[[package]]
name = "rstest_macros"
-version = "0.18.1"
+version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225e674cf31712b8bb15fdbca3ec0c1b9d825c5a24407ff2b7e005fb6a29ba03"
+checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605"
dependencies = [
"cfg-if",
"glob",
@@ -900,11 +900,11 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.7"
+version = "0.38.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399"
+checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
dependencies = [
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
@@ -995,9 +995,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "syn"
-version = "2.0.28"
+version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
@@ -1006,9 +1006,9 @@ dependencies = [
[[package]]
name = "tempfile"
-version = "3.7.1"
+version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651"
+checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
@@ -1019,18 +1019,18 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.44"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
+checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.44"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
+checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
@@ -1240,9 +1240,9 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.48.1"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
@@ -1255,45 +1255,45 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "xi-unicode"
diff --git a/Makefile.toml b/Makefile.toml
index 341a437..0d96586 100644
--- a/Makefile.toml
+++ b/Makefile.toml
@@ -173,6 +173,13 @@ install_crate = false
command = "cargo"
args = ["test", "--workspace"]
+[tasks.test-nightly]
+dependencies = ["update-rust-nightly"]
+toolchain = "nightly${RUST_NIGHTLY_VERSION_PREFIX}"
+install_crate = false
+command = "cargo"
+args = ["test", "--workspace"]
+
[tasks.update-rust-stable]
command = "rustup"
args = ["update", "stable"]
diff --git a/src/core/src/components/search_bar/mod.rs b/src/core/src/components/search_bar/mod.rs
index 9e1df81..da7d5fd 100644
--- a/src/core/src/components/search_bar/mod.rs
+++ b/src/core/src/components/search_bar/mod.rs
@@ -123,10 +123,6 @@ impl SearchBar {
self.state == State::Editing
}
- pub(crate) fn is_searching(&self) -> bool {
- self.state == State::Searching
- }
-
pub(crate) fn build_view_line(&self) -> ViewLine {
ViewLine::from(self.editable_line.line_segments())
}
diff --git a/src/core/src/components/search_bar/options.rs b/src/core/src/components/search_bar/options.rs
deleted file mode 100644
index 348d81c..0000000
--- a/src/core/src/components/search_bar/options.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use input::Event;
-
-pub struct Options {
- pub(crate) next_result_event: Vec<Event>,
- pub(crate) previous_result_event: Vec<Event>,
-}
-
-impl Options {
- pub fn new(next_result_event: Vec<Event>, previous_result_event: Vec<Event>) -> Self {
- Self {
- next_result_event,
- previous_result_event,
- }
- }
-}
diff --git a/src/core/src/components/search_bar/tests.rs b/src/core/src/components/search_bar/tests.rs
index 96619f0..7ae337e 100644
--- a/src/core/src/components/search_bar/tests.rs
+++ b/src/core/src/components/search_bar/tests.rs
@@ -245,17 +245,6 @@ fn is_editing() {
}
#[test]
-fn is_searching() {
- let mut search_bar = SearchBar::new();
- search_bar.state = State::Editing;
- assert!(!search_bar.is_searching());
- search_bar.state = State::Deactivated;
- assert!(!search_bar.is_searching());
- search_bar.state = State::Searching;
- assert!(search_bar.is_searching());
-}
-
-#[test]
fn build_view_line() {
assert_rendered_output!(
Options AssertRenderOptions::INCLUDE_STYLE | AssertRenderOptions::BODY_ONLY,
diff --git a/src/core/src/modules/list/mod.rs b/src/core/src/modules/list/mod.rs
index f89b1ac..4e6a9b8 100644
--- a/src/core/src/modules/list/mod.rs
+++ b/src/core/src/modules/list/mod.rs
@@ -1,34 +1,40 @@
+mod search;
#[cfg(all(unix, test))]
mod tests;
mod utils;
use std::{cmp::min, sync::Arc};
+use bitflags::Flags;
use captur::capture;
use config::Config;
use display::DisplayColor;
-use if_chain::if_chain;
use input::{InputOptions, MouseEventKind, StandardEvent};
use parking_lot::Mutex;
-use todo_file::{Action, EditContext, Line, Search, TodoFile};
+use todo_file::{Action, EditContext, Line, TodoFile};
use view::{LineSegment, RenderContext, ViewData, ViewLine};
-use self::utils::{
- get_list_normal_mode_help_lines,
- get_list_visual_mode_help_lines,
- get_todo_line_segments,
- TodoLineSegmentsOptions,
+use self::{
+ search::Search,
+ utils::{
+ get_line_action_maximum_width,
+ get_list_normal_mode_help_lines,
+ get_list_visual_mode_help_lines,
+ get_todo_line_segments,
+ TodoLineSegmentsOptions,
+ },
};
use crate::{
components::{
edit::Edit,
help::Help,
search_bar::{SearchBar, SearchBarAction},
+ spin_indicator::SpinIndicator,
},
events::{Event, KeyBindings, MetaEvent},
module::{ExitStatus, Module, State},
- modules::list::utils::get_line_action_maximum_width,
process::Results,
+ search::Searchable,
select,
};
@@ -58,9 +64,10 @@ pub(crate) struct List {
edit: Edit,
height: usize,
normal_mode_help: Help,
- search: Search,
search_bar: SearchBar,
+ search: Search,
selected_line_action: Option<Action>,
+ spin_indicator: SpinIndicator,
state: ListState,
todo_file: Arc<Mutex<TodoFile>>,
view_data: ViewData,
@@ -71,7 +78,12 @@ pub(crate) struct List {
impl Module for List {
fn activate(&mut self, _: State) -> Results {
self.selected_line_action = self.todo_file.lock().get_selected_line().map(|line| *line.get_action());
- Results::new()
+ let searchable: Box<dyn Searchable> = Box::new(self.search.clone());
+ let mut results = Results::from(searchable);
+ if let Some(term) = self.search_bar.search_value() {
+ results.search_term(term);
+ }
+ results
}
fn build_view_data(&mut self, context: &RenderContext) -> &ViewData {
@@ -141,14 +153,17 @@ impl List {
updater.set_show_help(true);
});
+ let search = Search::new(Arc::clone(&todo_file));
+
Self {
auto_select_next: config.auto_select_next,
edit: Edit::new(),
height: 0,
normal_mode_help: Help::new_from_keybindings(&get_list_normal_mode_help_lines(&config.key_bindings)),
- search: Search::new(),
+ search,
search_bar: SearchBar::new(),
selected_line_action: None,
+ spin_indicator: SpinIndicator::new(),
state: ListState::Normal,
todo_file,
view_data,
@@ -302,7 +317,7 @@ impl List {
#[allow(clippy::unused_self)]
fn open_in_editor(&mut self, results: &mut Results) {
- self.search_bar.reset();
+ results.search_cancel();
results.state(State::ExternalEditor);
}
@@ -318,7 +333,20 @@ impl List {
}
fn search_start(&mut self) {
- self.search_bar.start_search(None);
+ self.search_bar.start_search(Some(""));
+ }
+
+ fn search_update(&mut self) {
+ self.spin_indicator.refresh();
+ // select the first match, if it is available and has not been previously selected
+ if let Some(selected) = self.search.current_match() {
+ _ = self.update_cursor(CursorUpdate::Set(selected.index()));
+ }
+ else if !self.search_bar.is_editing() {
+ if let Some(selected) = self.search.next() {
+ _ = self.update_cursor(CursorUpdate::Set(selected));
+ }
+ }
}
fn help(&mut self) {
@@ -389,6 +417,7 @@ impl List {
if let Some(selected_line) = todo_file.get_selected_line() {
if selected_line.is_editable() {
self.state = ListState::Edit;
+ self.edit.reset();
self.edit.set_content(selected_line.get_content());
self.edit.set_label(format!("{} ", selected_line.get_action()).as_str());
}
@@ -405,11 +434,14 @@ impl List {
let is_visual_mode = self.state == ListState::Visual;
let selected_index = todo_file.get_selected_line_index();
let visual_index = self.visual_index_start.unwrap_or(selected_index);
+
let search_view_line = self.search_bar.is_editing().then(|| self.search_bar.build_view_line());
- let search_results_total = self.search_bar.is_searching().then(|| self.search.total_results());
+ let search_results_total = self.search.total_results();
let search_results_current = self.search.current_result_selected();
let search_term = self.search_bar.search_value();
let search_index = self.search.current_match();
+ let search_active = self.search.is_active();
+ let spin_indicator = self.spin_indicator.indicator();
self.view_data.update_view_data(|updater| {
capture!(todo_file);
@@ -423,6 +455,7 @@ impl List {
else {
let maximum_action_width = get_line_action_maximum_width(&todo_file);
for (index, line) in todo_file.lines_iter().enumerate() {
+ let search_match = self.search.match_at_index(index);
let selected_line = is_visual_mode
&& ((visual_index <= selected_index && index >= visual_index && index <= selected_index)
|| (visual_index > selected_index && index >= selected_index && index <= visual_index));
@@ -436,11 +469,17 @@ impl List {
if context.is_full_width() {
todo_line_segment_options.insert(TodoLineSegmentsOptions::FULL_WIDTH);
}
- if search_index.map_or(false, |v| v == index) {
+ if search_index.map_or(false, |v| v.index() == index) {
todo_line_segment_options.insert(TodoLineSegmentsOptions::SEARCH_LINE);
}
let mut view_line = ViewLine::new_with_pinned_segments(
- get_todo_line_segments(line, search_term, todo_line_segment_options, maximum_action_width),
+ get_todo_line_segments(
+ line,
+ search_term,
+ search_match,
+ todo_line_segment_options,
+ maximum_action_width,
+ ),
if line.has_reference() { 2 } else { 3 },
)
.set_selected(selected_index == index || selected_line);
@@ -457,17 +496,23 @@ impl List {
else if let Some(s_term) = search_term {
let mut search_line_segments = vec![];
search_line_segments.push(LineSegment::new(format!("[{s_term}]: ").as_str()));
- if_chain! {
- if let Some(s_total) = search_results_total;
- if let Some(s_index) = search_results_current;
- if s_total != 0;
- then {
- search_line_segments.push(LineSegment::new(format!("{}/{s_total}", s_index + 1).as_str()));
- }
- else {
- search_line_segments.push(LineSegment::new("No Results"));
- }
+
+ if search_results_total == 0 && !search_active {
+ search_line_segments.push(LineSegment::new("No Results"));
+ }
+ else if let Some(s_index) = search_results_current {
+ search_line_segments.push(LineSegment::new(
+ format!("{}/{search_results_total}", s_index + 1).as_str(),
+ ));
+ }
+ else {
+ search_line_segments.push(LineSegment::new(format!("-/{search_results_total}").as_str()));
+ }
+
+ if search_active {
+ search_line_segments.push(LineSegment::new(format!(" Searching [{spin_indicator}]").as_str()));
}
+
updater.push_trailing_line(ViewLine::from(search_line_segments));
}
}
@@ -568,34 +613,47 @@ impl List {
}
fn handle_search_input(&mut self, event: Event) -> Option<Results> {
- if self.search_bar.is_active() {
- let todo_file = self.todo_file.lock();
- match self.search_bar.handle_event(event) {
- SearchBarAction::Start(term) => {
- if term.is_empty() {
- self.search.cancel();
- self.search_bar.reset();
- }
- else {
- self.search.next(&todo_file, term.as_str());
- }
- },
- SearchBarAction::Next(term) => self.search.next(&todo_file, term.as_str()),
- SearchBarAction::Previous(term) => self.search.previous(&todo_file, term.as_str()),
- SearchBarAction::Cancel => {
- self.search.cancel();
- return Some(Results::from(event));
- },
- SearchBarAction::None | SearchBarAction::Update(_) => return None,
- }
- drop(todo_file);
-
- if let Some(selected) = self.search.current_match() {
- _ = self.update_cursor(CursorUpdate::Set(selected));
- }
- return Some(Results::from(event));
+ if !self.search_bar.is_active() {
+ return None;
+ }
+ let mut results = Results::from(event);
+ let todo_file = self.todo_file.lock();
+ match self.search_bar.handle_event(event) {
+ SearchBarAction::Update(term) => {
+ if term.is_empty() {
+ results.search_cancel();
+ }
+ else {
+ results.search_term(term.as_str());
+ }
+ },
+ SearchBarAction::Start(term) => {
+ if term.is_empty() {
+ results.search_cancel();
+ self.search_bar.reset();
+ }
+ else {
+ results.search_term(term.as_str());
+ }
+ },
+ SearchBarAction::Next(term) => {
+ results.search_term(term.as_str());
+ _ = self.search.next();
+ },
+ SearchBarAction::Previous(term) => {
+ results.search_term(term.as_str());
+ _ = self.search.previous();
+ },
+ SearchBarAction::Cancel => {
+ results.search_cancel();
+ return Some(results);
+ },
+ SearchBarAction::None => return Some(results),
}
- None
+ drop(todo_file);
+
+ self.search_update();
+ Some(results)
}
#[allow(clippy::integer_division)]
@@ -639,6 +697,7 @@ impl List {
MetaEvent::SwapSelectedDown => self.swap_selected_down(),
MetaEvent::SwapSelectedUp => self.swap_selected_up(),
MetaEvent::ToggleVisualMode => self.toggle_visual_mode(),
+ MetaEvent::SearchUpdate => self.search_update(),
_ => return None,
}
},
diff --git a/src/core/src/modules/list/search/line_match.rs b/src/core/src/modules/list/search/line_match.rs
new file mode 100644
index 0000000..f9618cd
--- /dev/null
+++ b/src/core/src/modules/list/search/line_match.rs
@@ -0,0 +1,24 @@
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub(crate) struct LineMatch {
+ index: usize,
+ hash: bool,
+ content: bool,
+}
+
+impl LineMatch {
+ pub(crate) const fn new(index: usize, hash: bool, content: bool) -> Self {
+ Self { index, hash, content }
+ }
+
+ pub(crate) const fn index(&self) -> usize {
+ self.index
+ }
+
+ pub(crate) const fn hash(&self) -> bool {
+ self.hash
+ }
+
+ pub(crate) const fn content(&self) -> bool {
+ self.content
+ }
+}
diff --git a/src/core/src/modules/list/search/mod.rs b/src/core/src/modules/list/search/mod.rs
new file mode 100644
index 0000000..0bf7ea3
--- /dev/null
+++ b/src/core/src/modules/list/search/mod.rs
@@ -0,0 +1,6 @@
+mod line_match;
+#[allow(clippy::module_inception)]
+mod search;
+mod state;
+
+pub(crate) use self::{line_match::LineMatch, search::Search, state::State};
diff --git a/src/core/src/modules/list/search/search.rs b/src/core/src/modules/list/search/search.rs
new file mode 100644
index 0000000..3773ece
--- /dev/null
+++ b/src/core/src/modules/list/search/search.rs
@@ -0,0 +1,750 @@
+use std::{
+ sync::{
+ atomic::{AtomicUsize, Ordering},
+ Arc,
+ },
+ time::Duration,
+};
+
+use parking_lot::{Mutex, RwLock};
+use todo_file::{Action, TodoFile};
+
+use super::{LineMatch, State};
+use crate::search::{Interrupter, SearchResult, SearchState, Searchable};
+
+const LOCK_DURATION: Duration = Duration::from_millis(100);
+
+#[derive(Clone, Debug)]
+pub(crate) struct Search {
+ cursor: Arc<AtomicUsize>,
+ state: Arc<RwLock<State>>,
+ todo_file: Arc<Mutex<TodoFile>>,
+}
+
+impl Searchable for Search {
+ fn reset(&mut self) {
+ self.state.write().reset();
+ }
+
+ fn search(&mut self, interrupter: Interrupter, term: &str) -> SearchResult {
+ let Some(todo_file) = self.todo_file.try_lock_for(LOCK_DURATION)
+ else {
+ return SearchResult::None;
+ };
+ let Some(mut state) = self.state.try_write_for(LOCK_DURATION)
+ else {
+ return SearchResult::None;
+ };
+ if state.try_invalidate_search(todo_file.version(), term) {
+ self.cursor.store(0, Ordering::Release);
+ }
+ let mut has_matches = false;
+ let mut complete = false;
+
+ state.set_search_state(SearchState::Active);
+ let mut cursor = self.cursor.load(Ordering::Acquire);
+ while interrupter.should_continue() {
+ let Some(line) = todo_file.get_line(cursor)
+ else {
+ complete = true;
+ break;
+ };
+
+ let action = *line.get_action();
+
+ let has_hash_match = match action {
+ Action::Break | Action::Noop | Action::Label | Action::Reset | Action::Merge | Action::Exec => false,
+ Action::Drop
+ | Action::Edit
+ | Action::Fixup
+ | Action::Pick
+ | Action::Reword
+ | Action::Squash
+ | Action::UpdateRef => line.get_hash().starts_with(term),
+ };
+ let has_content_match = match action {
+ Action::Break | Action::Noop => false,
+ Action::Drop
+ | Action::Edit
+ | Action::Fixup
+ | Action::Pick
+ | Action::Reword
+ | Action::Squash
+ | Action::UpdateRef
+ | Action::Label
+ | Action::Reset
+ | Action::Merge
+ | Action::Exec => line.get_content().contains(term),
+ };
+
+ has_matches = state.push_match(LineMatch::new(cursor, has_hash_match, has_content_match)) || has_matches;
+
+ cursor += 1;
+ }
+
+ self.cursor.store(cursor, Ordering::Release);
+
+ if has_matches {
+ SearchResult::Updated
+ }
+ else if complete {
+ state.set_search_state(SearchState::Complete);
+ SearchResult::Complete
+ }
+ else {
+ SearchResult::None
+ }
+ }
+}
+
+impl Search {
+ /// Create a new instance
+ #[inline]
+ #[must_use]
+ pub(crate) fn new(todo_file: Arc<Mutex<TodoFile>>) -> Self {
+ Self {
+ cursor: Arc::new(AtomicUsize::new(0)),
+ state: Arc::new(RwLock::new(State::new())),
+ todo_file,
+ }
+ }
+
+ /// Select the next search result
+ #[inline]
+ #[allow(clippy::missing_panics_doc)]
+ pub(crate) fn next(&mut self) -> Option<usize> {
+ let mut state = self.state.write();
+
+ if state.matches().is_empty() {
+ return None;
+ }
+
+ let new_selected = if let Some(mut current) = state.selected() {
+ current += 1;
+ if current >= state.number_matches() { 0 } else { current }
+ }
+ else {
+ // select the line after the hint that matches
+ let mut index_match = 0;
+ for (i, v) in state.matches().iter().copied().enumerate() {
+ if v.index() >= state.match_start_hint() {
+ index_match = i;
+ break;
+ }
+ }
+ index_match
+ };
+ state.set_selected(new_selected);
+
+ let new_match_hint = state.match_value(new_selected).map_or(0, |s| s.index());
+ state.set_match_start_hint(new_match_hint);
+ Some(new_match_hint)
+ }
+
+ /// Select the previous search result
+ #[inline]
+ #[allow(clippy::missing_panics_doc)]
+ pub(crate) fn previous(&mut self) -> Option<usize> {
+ let mut state = self.state.write();
+ if state.matches().is_empty() {
+ return None;
+ }
+
+ let new_selected = if let Some(current) = state.selected() {
+ if current == 0 {
+ state.number_matches().saturating_sub(1)
+ }
+ else {
+ current.saturating_sub(1)
+ }
+ }
+ else {
+ // select the line previous to hint that matches
+ let mut index_match = state.number_matches().saturating_sub(1);
+ for (i, v) in state.matches().iter().copied().enumerate().rev() {
+ if v.index() <= state.match_start_hint() {
+ index_match = i;
+ break;
+ }
+ }
+ index_match
+ };
+ state.set_selected(new_selected);
+
+ let new_match_hint = state.match_value(new_selected).map_or(0, |s| s.index());
+ state.set_match_start_hint(new_match_hint);
+ Some(new_match_hint)
+ }
+
+ /// Set a hint for which result to select first during search
+ #[inline]
+ pub(crate) fn set_search_start_hint(&mut self, hint: usize) {
+ self.state.write().set_match_start_hint(hint);
+ }
+
+ /// Get the index of the current selected result, if there is one
+ #[inline]
+ #[must_use]
+ pub(crate) fn current_match(&self) -> Option<LineMatch> {
+ let state = self.state.read();
+ let selected = state.selected()?;
+ state.match_value(selected)
+ }
+
+ /// Get the index of the current selected result, if there is one
+ #[inline]
+ #[must_use]
+ pub(crate) fn match_at_index(&self, index: usize) -> Option<LineMatch> {
+ self.state.read().match_value_for_line(index)
+ }
+
+ /// Get the selected result number, if there is one
+ #[inline]
+ #[must_use]
+ pub(crate) fn current_result_selected(&self) -> Option<usize> {
+ self.state.read().selected()
+ }
+
+ /// Get the total number of results
+ #[inline]
+ #[must_use]
+ pub(crate) fn total_results(&self) -&