From 825523ada89134c3637af3127af1c9c8e9cd1b7c Mon Sep 17 00:00:00 2001 From: Louis Thevenet Date: Sun, 29 Sep 2024 21:30:10 +0200 Subject: [PATCH] feat(tui): add filter tab (#13) * feat(core): add task filter * refactor: rename tab components * feat(app): send only key event actions if a component is in editing mode * feat(explorer): if not focused, handle only tab events * wip: filter tab can filter tasks * chore: improve test vault * feat(filter): non case sensitive search and able to search by task state * feat(tui): add hint on keybindings in footer * fix(filter): accept if task's tags contain searched tags as substring * feat(filter_tab): add a list of tags found in the vault, which updates during search * chore: fix clippy warnings --- .config/config.json5 | 4 +- Cargo.lock | 11 + Cargo.toml | 1 + src/action.rs | 4 +- src/app.rs | 18 +- src/components.rs | 12 +- .../{explorer.rs => explorer_tab.rs} | 48 ++- src/components/filter_tab.rs | 205 +++++++++ src/components/home.rs | 8 +- src/components/tags.rs | 55 --- src/task_core.rs | 45 +- src/task_core/filter.rs | 389 ++++++++++++++++++ test-vault/test/test.md | 20 +- test-vault/test/test2.md | 8 +- test-vault/test/test3.md | 4 +- 15 files changed, 724 insertions(+), 108 deletions(-) rename src/components/{explorer.rs => explorer_tab.rs} (87%) create mode 100644 src/components/filter_tab.rs delete mode 100644 src/components/tags.rs create mode 100644 src/task_core/filter.rs diff --git a/.config/config.json5 b/.config/config.json5 index 7ca76a4..7568b58 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -17,7 +17,7 @@ "":"TabLeft", "":"TabLeft", }, - "Tags": { + "Filter": { "": "Quit", "": "Quit", "": "Quit", @@ -27,6 +27,8 @@ "":"TabRight", "":"TabLeft", "":"TabLeft", + + "": "Enter", }, "Explorer": { "": "Quit", diff --git a/Cargo.lock b/Cargo.lock index 46d4718..fd65df3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2528,6 +2528,16 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tui-input" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" +dependencies = [ + "ratatui", + "unicode-width", +] + [[package]] name = "tui-widget-list" version = "0.12.1" @@ -2671,6 +2681,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "tui-input", "tui-widget-list", "vergen-gix", "winnow", diff --git a/Cargo.toml b/Cargo.toml index 4017109..f85dea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ chrono = "0.4.38" toml = "0.8.19" winnow = "0.6.18" tui-widget-list = "0.12.1" +tui-input = "0.10.1" [build-dependencies] anyhow = "1.0.86" diff --git a/src/action.rs b/src/action.rs index dffdec4..0e149cf 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,3 +1,4 @@ +use crossterm::event::KeyEvent; use serde::{Deserialize, Serialize}; use strum::Display; @@ -12,6 +13,7 @@ pub enum Action { ClearScreen, Error(String), Help, + Key(KeyEvent), Up, Down, Left, @@ -21,5 +23,5 @@ pub enum Action { TabRight, TabLeft, FocusExplorer, - FocusTags, + FocusFilter, } diff --git a/src/app.rs b/src/app.rs index 5dfd440..10e1919 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,9 @@ use tracing::{debug, info}; use crate::{ action::Action, - components::{explorer::Explorer, fps::FpsCounter, home::Home, tags::Tags, Component}, + components::{ + explorer_tab::ExplorerTab, filter_tab::FilterTab, fps::FpsCounter, home::Home, Component, + }, config::Config, tui::{Event, Tui}, }; @@ -30,7 +32,7 @@ pub enum Mode { #[default] Home, Explorer, - Tags, + Filter, } impl App { @@ -42,8 +44,8 @@ impl App { components: vec![ Box::new(Home::new()), Box::::default(), - Box::new(Explorer::new()), - Box::new(Tags::new()), + Box::new(ExplorerTab::new()), + Box::new(FilterTab::new()), ], should_quit: false, should_suspend: false, @@ -117,6 +119,12 @@ impl App { let Some(keymap) = self.config.keybindings.get(&self.mode) else { return Ok(()); }; + + if self.components.iter().any(|c| c.editing_mode()) { + action_tx.send(Action::Key(key))?; + return Ok(()); + } + if let Some(action) = keymap.get(&vec![key]) { action_tx.send(action.clone())?; } else { @@ -140,7 +148,7 @@ impl App { } match action { Action::FocusExplorer => self.mode = Mode::Explorer, - Action::FocusTags => self.mode = Mode::Tags, + Action::FocusFilter => self.mode = Mode::Filter, Action::Tick => { self.last_tick_key_events.drain(..); } diff --git a/src/components.rs b/src/components.rs index 8fc349c..d1b3224 100644 --- a/src/components.rs +++ b/src/components.rs @@ -8,10 +8,10 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{action::Action, config::Config, tui::Event}; -pub mod explorer; +pub mod explorer_tab; +pub mod filter_tab; pub mod fps; pub mod home; -pub mod tags; /// `Component` is a trait that represents a visual and interactive element of the user interface. /// @@ -124,4 +124,12 @@ pub trait Component { /// /// * `Result<()>` - An Ok result or an error. fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>; + /// Whether the app should send Actions or `Action::Key` + /// + /// # Returns + /// + /// * `bool` - Whether the component is in editing mode or not. + fn editing_mode(&self) -> bool { + false + } } diff --git a/src/components/explorer.rs b/src/components/explorer_tab.rs similarity index 87% rename from src/components/explorer.rs rename to src/components/explorer_tab.rs index f4b3805..bf78fa2 100644 --- a/src/components/explorer.rs +++ b/src/components/explorer_tab.rs @@ -14,7 +14,7 @@ use crate::widgets::task_list::TaskList; use crate::{action::Action, config::Config}; #[derive(Default)] -pub struct Explorer { +pub struct ExplorerTab { command_tx: Option>, config: Config, focused: bool, @@ -27,7 +27,7 @@ pub struct Explorer { entries_right_view: Vec, } -impl Explorer { +impl ExplorerTab { pub fn new() -> Self { Self::default() } @@ -155,9 +155,14 @@ impl Explorer { .map(|item| format!("{} {}", item.0, item.1)) .collect() } + pub fn render_footer(area: Rect, frame: &mut Frame) { + Line::raw("Press hjkl|◄▼▲▶ to move") + .centered() + .render(area, frame.buffer_mut()); + } } -impl Component for Explorer { +impl Component for ExplorerTab { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) @@ -172,21 +177,24 @@ impl Component for Explorer { } fn update(&mut self, action: Action) -> Result> { - match action { - Action::FocusExplorer => self.focused = true, - Action::FocusTags => self.focused = false, - Action::Up => { - self.state_center_view.previous(); - self.update_preview(); - } - Action::Down => { - self.state_center_view.next(); - self.update_preview(); + if self.focused { + match action { + Action::FocusFilter => self.focused = false, + Action::Up => { + self.state_center_view.previous(); + self.update_preview(); + } + Action::Down => { + self.state_center_view.next(); + self.update_preview(); + } + Action::Right | Action::Enter => self.enter_selected_entry()?, + Action::Left | Action::Cancel => self.leave_selected_entry()?, + Action::Help => todo!(), + _ => (), } - Action::Right | Action::Enter => self.enter_selected_entry()?, - Action::Left | Action::Cancel => self.leave_selected_entry()?, - Action::Help => todo!(), - _ => (), + } else if action == Action::FocusExplorer { + self.focused = true; } Ok(None) } @@ -205,8 +213,12 @@ impl Component for Explorer { Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), + Constraint::Length(1), ]); - let [_header_area, inner_area, _footer_areaa] = vertical.areas(frame.area()); + let [_header_area, inner_area, footer_area, _tab_footer_areaa] = + vertical.areas(frame.area()); + + Self::render_footer(footer_area, frame); // Outer Layout : path on top, main layout on bottom let outer_layout = Layout::default() diff --git a/src/components/filter_tab.rs b/src/components/filter_tab.rs new file mode 100644 index 0000000..f5e084d --- /dev/null +++ b/src/components/filter_tab.rs @@ -0,0 +1,205 @@ +use color_eyre::Result; +use crossterm::event::{Event, KeyCode}; +use ratatui::widgets::{List, Paragraph}; +use ratatui::{prelude::*, widgets::Block}; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; + +use crate::task_core::filter::filter; +use crate::task_core::parser::task::parse_task; +use crate::task_core::task::Task; +use crate::task_core::vault_data::VaultData; +use crate::task_core::TaskManager; +use crate::widgets::task_list::TaskList; +use crate::{action::Action, config::Config}; +use tui_input::{backend::crossterm::EventHandler, Input}; + +#[derive(Default)] +pub struct FilterTab { + command_tx: Option>, + config: Config, + focused: bool, + input: Input, + input_mode: InputMode, + matching_entries: Vec, + matching_tags: Vec, + task_mgr: TaskManager, +} + +#[derive(Default)] +enum InputMode { + Normal, + #[default] + Editing, +} +impl InputMode { + const fn invert(&self) -> Self { + match self { + Self::Normal => Self::Editing, + Self::Editing => Self::Normal, + } + } +} +impl FilterTab { + pub fn new() -> Self { + Self::default() + } + pub fn render_footer(&self, area: Rect, frame: &mut Frame) { + match self.input_mode { + InputMode::Normal => Line::raw("Press Enter to start searching"), + + InputMode::Editing => Line::raw("Press Enter to stop searching"), + } + .centered() + .render(area, frame.buffer_mut()); + } + fn update_matching_entries(&mut self) { + let has_state = self.input.value().starts_with("- ["); + let input_value = format!( + "{}{}", + if has_state { "" } else { "- [ ]" }, + self.input.value() + ); + let search = match parse_task(&mut input_value.as_str(), &self.config) { + Ok(t) => t, + Err(_e) => { + self.matching_entries = vec![Task { + name: String::from("Uncomplete search prompt"), + ..Default::default() + }]; + return; + } + }; + + self.matching_entries = filter(&self.task_mgr.tasks, &search, has_state); + + self.matching_tags = if search.tags.is_none() { + self.task_mgr.tags.iter().cloned().collect::>() + } else { + let search_tags = search.tags.unwrap_or_default(); + self.task_mgr + .tags + .iter() + .filter(|t| search_tags.clone().iter().any(|t2| t.contains(t2))) + .cloned() + .collect() + }; + self.matching_tags.sort(); + } +} +impl Component for FilterTab { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.task_mgr = TaskManager::load_from_config(&config)?; + self.config = config; + self.update_matching_entries(); + Ok(()) + } + + fn editing_mode(&self) -> bool { + self.focused + && match self.input_mode { + InputMode::Normal => false, + InputMode::Editing => true, + } + } + fn update(&mut self, action: Action) -> Result> { + if self.focused { + match action { + Action::FocusExplorer => self.focused = false, + Action::FocusFilter => self.focused = true, + Action::Enter => self.input_mode = self.input_mode.invert(), + Action::Key(key) if matches!(self.input_mode, InputMode::Editing) => match key.code + { + KeyCode::Enter | KeyCode::Esc => self.input_mode = self.input_mode.invert(), + _ => { + self.input.handle_event(&Event::Key(key)); + self.update_matching_entries(); + } + }, + _ => (), + } + } else { + match action { + Action::FocusExplorer => self.focused = false, + Action::FocusFilter => self.focused = true, + _ => (), + } + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, _area: Rect) -> Result<()> { + if !self.focused { + return Ok(()); + } + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(1), + Constraint::Length(1), + ]); + let [_header_area, search_area, content_area, footer_area, _tab_footer_areaa] = + vertical.areas(frame.area()); + + self.render_footer(footer_area, frame); + + let width = search_area.width.max(3) - 3; // 2 for borders, 1 for cursor + let scroll = self.input.visual_scroll(width as usize); + match self.input_mode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + frame.set_cursor_position(( + // Put cursor past the end of the input text + search_area + .x + .saturating_add(((self.input.visual_cursor()).max(scroll) - scroll) as u16) + + 1, + // Move one line down, from the border to the input line + search_area.y + 1, + )); + } + } + + let input = + Paragraph::new(self.input.value()) + .style(Style::reset()) + .block(Block::bordered().title("Input").style(Style::new().fg( + match self.input_mode { + InputMode::Editing => Color::Rgb(255, 153, 0), + InputMode::Normal => Color::default(), + }, + ))) + .scroll((0, scroll as u16)); + frame.render_widget(input, search_area); + + let [tag_area, list_area] = + Layout::horizontal([Constraint::Length(15), Constraint::Min(0)]).areas(content_area); + + let tag_list = List::new(self.matching_tags.iter().map(std::string::String::as_str)) + .block(Block::bordered().title("Found Tags")); + + let entries_list = TaskList::new( + &self.config, + &self + .matching_entries + .clone() + .iter() + .map(|t| VaultData::Task(t.clone())) + .collect::>(), + ); + Widget::render(tag_list, tag_area, frame.buffer_mut()); + entries_list.render(list_area, frame.buffer_mut()); + Ok(()) + } +} diff --git a/src/components/home.rs b/src/components/home.rs index 2534902..b2c7192 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -22,7 +22,7 @@ impl Home { if let Some(tx) = &self.command_tx { if let Err(e) = tx.send(match self.selected_tab { SelectedTab::Explorer => Action::FocusExplorer, - SelectedTab::Tags => Action::FocusTags, + SelectedTab::Filter => Action::FocusFilter, }) { error!("Error while changing sending new focused tab information: {e}"); } @@ -52,7 +52,7 @@ impl Home { } pub fn render_footer(area: Rect, frame: &mut Frame) { - Line::raw("Ctrl + ◄ | ► to change tab | Press q to quit") + Line::raw("Ctrl+◄► to change tab | Press q to quit") .centered() .render(area, frame.buffer_mut()); } @@ -94,8 +94,8 @@ enum SelectedTab { #[default] #[strum(to_string = "Explorer")] Explorer, - #[strum(to_string = "Tags")] - Tags, + #[strum(to_string = "Filter")] + Filter, } impl SelectedTab { diff --git a/src/components/tags.rs b/src/components/tags.rs deleted file mode 100644 index 7b66ccd..0000000 --- a/src/components/tags.rs +++ /dev/null @@ -1,55 +0,0 @@ -use color_eyre::Result; -use ratatui::prelude::*; -use ratatui::widgets::Paragraph; -use tokio::sync::mpsc::UnboundedSender; - -use super::Component; - -use crate::{action::Action, config::Config}; - -#[derive(Default)] -pub struct Tags { - command_tx: Option>, - config: Config, - focused: bool, -} - -impl Tags { - pub fn new() -> Self { - Self::default() - } -} -impl Component for Tags { - fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { - self.command_tx = Some(tx); - Ok(()) - } - - fn register_config_handler(&mut self, config: Config) -> Result<()> { - self.config = config; - Ok(()) - } - - fn update(&mut self, action: Action) -> Result> { - match action { - Action::FocusExplorer => self.focused = false, - Action::FocusTags => self.focused = true, - _ => (), - } - Ok(None) - } - - fn draw(&mut self, frame: &mut Frame, _area: Rect) -> Result<()> { - if !self.focused { - return Ok(()); - } - let vertical = Layout::vertical([ - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(1), - ]); - let [_header_area, inner_area, _footer_areaa] = vertical.areas(frame.area()); - Paragraph::new("tags").render(inner_area, frame.buffer_mut()); - Ok(()) - } -} diff --git a/src/task_core.rs b/src/task_core.rs index d78a441..4d30d12 100644 --- a/src/task_core.rs +++ b/src/task_core.rs @@ -1,6 +1,6 @@ use color_eyre::{eyre::bail, Result}; -use std::{cmp::Ordering, fmt::Display, path::PathBuf}; +use std::{cmp::Ordering, collections::HashSet, fmt::Display, path::PathBuf}; use vault_data::VaultData; use tracing::{debug, error}; @@ -8,7 +8,8 @@ use vault_parser::VaultParser; use crate::config::Config; -mod parser; +pub mod filter; +pub mod parser; pub mod task; pub mod vault_data; mod vault_parser; @@ -18,11 +19,13 @@ pub const DIRECTORY_EMOJI: &str = "📁"; pub struct TaskManager { pub tasks: VaultData, + pub tags: HashSet, } impl Default for TaskManager { fn default() -> Self { Self { tasks: VaultData::Directory("Empty".to_owned(), vec![]), + tags: HashSet::new(), } } } @@ -35,8 +38,27 @@ impl TaskManager { Self::rewrite_vault_tasks(config, &tasks) .unwrap_or_else(|e| error!("Failed to fix tasks' due dates: {e}")); + let mut tags = HashSet::new(); + Self::collect_tags(&tasks, &mut tags); debug!("\n{}", tasks); - Ok(Self { tasks }) + debug!("\n{:#?}", tags); + Ok(Self { tasks, tags }) + } + + fn collect_tags(tasks: &VaultData, tags: &mut HashSet) { + match tasks { + VaultData::Directory(_, children) | VaultData::Header(_, _, children) => { + children.iter().for_each(|c| Self::collect_tags(c, tags)); + } + VaultData::Task(task) => { + task.tags.clone().unwrap_or_default().iter().for_each(|t| { + tags.insert(t.clone()); + }); + task.subtasks + .iter() + .for_each(|task| Self::collect_tags(&VaultData::Task(task.clone()), tags)); + } + } } /// Recursively calls `Task.fix_task_attributes` on every task from the vault. @@ -274,6 +296,8 @@ impl Display for TaskManager { #[cfg(test)] mod tests { + use std::collections::HashSet; + use pretty_assertions::assert_eq; use crate::task_core::{task::Task, vault_data::VaultData}; @@ -327,7 +351,10 @@ mod tests { )], ); - let task_mgr = TaskManager { tasks: input }; + let task_mgr = TaskManager { + tasks: input, + tags: HashSet::new(), + }; let path = vec![String::from("Test"), String::from("1"), String::from("2")]; let expected = vec![(String::from("###"), String::from("3"))]; @@ -386,7 +413,10 @@ mod tests { )], ); - let task_mgr = TaskManager { tasks: input }; + let task_mgr = TaskManager { + tasks: input, + tags: HashSet::new(), + }; let path = vec![ String::from("Testaaa"), @@ -465,7 +495,10 @@ mod tests { )], ); - let task_mgr = TaskManager { tasks: input }; + let task_mgr = TaskManager { + tasks: input, + tags: HashSet::new(), + }; let path = vec![String::from("Test"), String::from("1"), String::from("2")]; let res = task_mgr.get_vault_data_from_path(&path).unwrap(); diff --git a/src/task_core/filter.rs b/src/task_core/filter.rs new file mode 100644 index 0000000..2440dfb --- /dev/null +++ b/src/task_core/filter.rs @@ -0,0 +1,389 @@ +use crate::task_core::task::DueDate; + +use super::{task::Task, vault_data::VaultData}; + +pub fn filter(vault_data: &VaultData, search: &Task, compare_states: bool) -> Vec { + fn aux(vault_data: &VaultData, search: &Task, compare_states: bool, res: &mut Vec) { + match vault_data { + VaultData::Directory(_, children) | VaultData::Header(_, _, children) => { + for c in children { + aux(&c.clone(), search, compare_states, res); + } + } + VaultData::Task(task) => { + let state_match = search.state == task.state; + + let name_match = if search.name.is_empty() { + true + } else { + task.name + .to_lowercase() + .contains(&search.name.to_lowercase()) + }; + + let date_match = match (task.due_date.clone(), search.due_date.clone()) { + (_, DueDate::NoDate) => true, + (DueDate::DayTime(task_date), DueDate::DayTime(search_date)) + if task_date == search_date => + { + true + } + (DueDate::Day(task_date), DueDate::Day(search_date)) + if task_date == search_date => + { + true + } + (_, _) => false, + }; + + let tags_match = search.tags.clone().unwrap_or_default().iter().all(|t| { + task.tags + .clone() + .unwrap_or_default() + .iter() + .any(|x| x.to_lowercase().contains(&t.to_lowercase())) + }); + + let priority_match = if search.priority > 0 { + search.priority == task.priority + } else { + true + }; + + if (!compare_states || state_match) + && name_match + && date_match + && tags_match + && priority_match + { + res.push(task.clone()); + } + + task.subtasks + .iter() + .for_each(|t| aux(&VaultData::Task(t.clone()), search, compare_states, res)); + } + } + } + let res = &mut vec![]; + aux(vault_data, search, compare_states, res); + res.clone() +} + +#[cfg(test)] +mod tests { + use chrono::NaiveDate; + + use crate::task_core::{ + task::{DueDate, Task}, + vault_data::VaultData, + }; + + use super::filter; + + #[test] + fn filter_tags_test() { + let input = VaultData::Directory( + "test".to_owned(), + vec![ + VaultData::Header( + 0, + "Test".to_string(), + vec![ + VaultData::Header( + 1, + "1".to_string(), + vec![VaultData::Header( + 2, + "2".to_string(), + vec![VaultData::Task(Task { + name: "test 1".to_string(), + line_number: 8, + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + )], + ), + VaultData::Header( + 1, + "1.2".to_string(), + vec![ + VaultData::Header(3, "3".to_string(), vec![]), + VaultData::Header( + 2, + "4".to_string(), + vec![VaultData::Task(Task { + name: "test 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + ), + ], + ), + ], + ), + VaultData::Task(Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }), + ], + ); + let expected = vec![ + Task { + name: "test 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }, + Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }, + ]; + let res = filter( + &input, + &Task { + name: String::new(), + tags: Some(vec!["test".to_string()]), + ..Default::default() + }, + false, + ); + assert_eq!(res, expected); + } + #[test] + fn filter_names_test() { + let input = VaultData::Directory( + "test".to_owned(), + vec![ + VaultData::Header( + 0, + "Test".to_string(), + vec![ + VaultData::Header( + 1, + "1".to_string(), + vec![VaultData::Header( + 2, + "2".to_string(), + vec![VaultData::Task(Task { + name: "hfdgqskhjfg1".to_string(), + line_number: 8, + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + )], + ), + VaultData::Header( + 1, + "1.2".to_string(), + vec![ + VaultData::Header(3, "3".to_string(), vec![]), + VaultData::Header( + 2, + "4".to_string(), + vec![VaultData::Task(Task { + name: "test 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + ), + ], + ), + ], + ), + VaultData::Task(Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }), + ], + ); + let expected = vec![ + Task { + name: "test 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }, + Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }, + ]; + let res = filter( + &input, + &Task { + name: String::from("test"), + ..Default::default() + }, + false, + ); + assert_eq!(res, expected); + } + #[test] + fn filter_due_date_test() { + let input = VaultData::Directory( + "test".to_owned(), + vec![ + VaultData::Header( + 0, + "Test".to_string(), + vec![ + VaultData::Header( + 1, + "1".to_string(), + vec![VaultData::Header( + 2, + "2".to_string(), + vec![VaultData::Task(Task { + name: "hfdgqskhjfg1".to_string(), + line_number: 8, + due_date: DueDate::Day( + NaiveDate::from_ymd_opt(2020, 2, 2).unwrap(), + ), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + )], + ), + VaultData::Header( + 1, + "1.2".to_string(), + vec![ + VaultData::Header(3, "3".to_string(), vec![]), + VaultData::Header( + 2, + "4".to_string(), + vec![VaultData::Task(Task { + name: "test 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + ), + ], + ), + ], + ), + VaultData::Task(Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }), + ], + ); + let expected = vec![Task { + name: "hfdgqskhjfg1".to_string(), + line_number: 8, + due_date: DueDate::Day(NaiveDate::from_ymd_opt(2020, 2, 2).unwrap()), + description: Some("test\ndesc".to_string()), + ..Default::default() + }]; + let res = filter( + &input, + &Task { + due_date: DueDate::Day(NaiveDate::from_ymd_opt(2020, 2, 2).unwrap()), + ..Default::default() + }, + false, + ); + assert_eq!(res, expected); + } + #[test] + fn filter_full_test() { + let input = VaultData::Directory( + "test".to_owned(), + vec![ + VaultData::Header( + 0, + "Test".to_string(), + vec![ + VaultData::Header( + 1, + "1".to_string(), + vec![VaultData::Header( + 2, + "2".to_string(), + vec![VaultData::Task(Task { + name: "real target".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + due_date: DueDate::Day( + NaiveDate::from_ymd_opt(2020, 2, 2).unwrap(), + ), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + )], + ), + VaultData::Header( + 1, + "1.2".to_string(), + vec![ + VaultData::Header(3, "3".to_string(), vec![]), + VaultData::Header( + 2, + "4".to_string(), + vec![VaultData::Task(Task { + name: "false target 2".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + })], + ), + ], + ), + ], + ), + VaultData::Task(Task { + name: "test 3".to_string(), + line_number: 8, + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }), + ], + ); + let expected = vec![Task { + name: "real target".to_string(), + line_number: 8, + due_date: DueDate::Day(NaiveDate::from_ymd_opt(2020, 2, 2).unwrap()), + tags: Some(vec!["test".to_string()]), + description: Some("test\ndesc".to_string()), + ..Default::default() + }]; + let res = filter( + &input, + &Task { + name: String::from("target"), + tags: Some(vec!["test".to_string()]), + due_date: DueDate::Day(NaiveDate::from_ymd_opt(2020, 2, 2).unwrap()), + ..Default::default() + }, + false, + ); + assert_eq!(res, expected); + } +} diff --git a/test-vault/test/test.md b/test-vault/test/test.md index 125439f..a51370f 100644 --- a/test-vault/test/test.md +++ b/test-vault/test/test.md @@ -1,10 +1,10 @@ # Header 1 -- [X] Test 2024/09/20 p2 -- [ ] Another task with tags 2024/09/21 #tag #tag2 - - [X] Subtask - - [ ] Subtask 2 +- [X] Test 2024/09/30 +- [ ] Another task with tags 2024/09/29 #tag #tag2 +- [X] Subtask +- [ ] Subtask 2 -- [ ] Task with description 2024/09/23 p6 #tag +- [ ] Task with description 2024/09/29 p1 #tag yay description multiline @@ -12,14 +12,14 @@ blablabla # Header 2 -- [ ] task here -- [ ] other task p2 +- [ ] task here p1 #tag +- [ ] other task 2024/09/29 p1 ## header 2.1 -- [ ] task here +- [ ] task here ### header 2.1.1 -- [ ] task here +- [ ] task here ## header 2.2 -- [X] task here +- [X] task here diff --git a/test-vault/test/test2.md b/test-vault/test/test2.md index 2eeb4ba..31241ae 100644 --- a/test-vault/test/test2.md +++ b/test-vault/test/test2.md @@ -1,10 +1,10 @@ # Header 1 ## Header 2 -- [ ] Content header 1->2 - - [ ] A subtask +- [ ] Content header 1->2 +- [X] A subtask ## Header 2 2 -- [ ] Content header 1->2 2 +- [X] Content header 1->2 2 #tag #tag1 # Header 1 1 -- [ ] Content Header 1 1 +- [ ] Content Header 1 1 #tag diff --git a/test-vault/test/test3.md b/test-vault/test/test3.md index d3764a5..a80465e 100644 --- a/test-vault/test/test3.md +++ b/test-vault/test/test3.md @@ -1,11 +1,11 @@ # 1 useless ## 2 useless ### 3 useless -- [ ] test +- [ ] test # 2 useful ### 3 useless ## 4 useful -- [ ] test +- [ ] test test desc