use std::collections::{HashMap, HashSet};
use chrono::{Local, NaiveDate};
use crate::api::types::{Block, DailyNote, LinkedRefBlock, LinkedRefGroup};
use crate::edit_buffer::EditBuffer;
use crate::error::ErrorPopup;
#[derive(Debug, Clone)]
pub enum UndoEntry {
TextEdit {
block_uid: String,
old_text: String,
},
CreateBlock {
block_uid: String,
},
DeleteBlock {
block: Block,
parent_uid: String,
order: i64,
selected_block: usize,
},
MoveBlock {
block_uid: String,
old_parent_uid: String,
old_order: i64,
selected_block: usize,
},
Batch(Vec<UndoEntry>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct AutocompleteState {
pub query: String,
pub results: Vec<(String, String)>, pub selected: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SearchState {
pub query: String,
pub results: Vec<(String, String)>, pub selected: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ViewMode {
DailyNotes,
Page { title: String },
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuickSwitcherState {
pub query: String,
pub filtered: Vec<(String, String)>, pub selected: usize,
pub debounce_ticks: u8,
pub fetching: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LinkPickerState {
pub links: Vec<String>,
pub selected: usize,
}
#[derive(Debug, Clone)]
pub(super) struct ViewSnapshot {
pub(super) view_mode: ViewMode,
pub(super) days: Vec<DailyNote>,
pub(super) selected_block: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LoadRequest {
DailyNote(NaiveDate),
Page(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct LinkedRefsState {
pub groups: Vec<LinkedRefGroup>,
pub collapsed: bool,
pub loading: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LinkedRefItem {
SectionHeader,
GroupHeader(String),
Block(LinkedRefBlock),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppMessage {
Key(crossterm::event::KeyEvent),
DailyNoteLoaded(DailyNote),
PageLoaded(DailyNote),
RefreshLoaded(DailyNote),
BlockRefResolved(String, String), LinkedRefsLoaded(String, Vec<LinkedRefGroup>), PageTitlesLoaded(Vec<(String, String)>), ApiError(crate::error::ErrorInfo),
Tick,
}
pub struct AppState {
pub graph_name: String,
pub date_display: String,
pub days: Vec<DailyNote>,
pub current_date: NaiveDate,
pub selected_block: usize,
pub selection: Selection,
pub cursor_col: usize,
pub loading: bool,
pub loading_more: bool,
pub status_message: Option<String>,
pub hints: Vec<(String, &'static str)>,
pub help_hints: Vec<(String, &'static str)>,
pub should_quit: bool,
pub refresh_counter: u32,
pub input_mode: InputMode,
pub pending_key: Option<char>,
pub block_ref_cache: HashMap<String, String>,
pub(super) pending_block_refs: HashSet<String>,
pub autocomplete: Option<AutocompleteState>,
pub search: Option<SearchState>,
pub undo_stack: Vec<UndoEntry>,
pub redo_stack: Vec<UndoEntry>,
pub show_help: bool,
pub view_mode: ViewMode,
pub(super) nav_history: Vec<ViewSnapshot>,
pub(super) nav_index: usize,
pub link_picker: Option<LinkPickerState>,
pub error_popup: Option<ErrorPopup>,
pub linked_refs: HashMap<String, LinkedRefsState>,
pub slash_menu: Option<super::slash::SlashMenuState>,
pub quick_switcher: Option<QuickSwitcherState>,
pub(super) page_title_cache: Vec<(String, String)>,
pub needs_linked_refs_refresh: bool,
pub(super) placeholder_uids: HashSet<String>,
}
impl AppState {
pub fn new(
graph_name: &str,
hints: Vec<(String, &'static str)>,
help_hints: Vec<(String, &'static str)>,
) -> Self {
let now = Local::now();
Self {
graph_name: graph_name.to_string(),
date_display: now.format("%b %d, %Y").to_string(),
days: Vec::new(),
current_date: now.date_naive(),
selected_block: 0,
selection: Selection::Single(0),
cursor_col: 0,
loading: true,
loading_more: false,
status_message: Some("Loading today's notes...".into()),
hints,
help_hints,
should_quit: false,
refresh_counter: 0,
input_mode: InputMode::Normal,
pending_key: None,
block_ref_cache: HashMap::new(),
pending_block_refs: HashSet::new(),
autocomplete: None,
search: None,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
show_help: false,
view_mode: ViewMode::DailyNotes,
nav_history: Vec::new(),
nav_index: 0,
link_picker: None,
error_popup: None,
linked_refs: HashMap::new(),
slash_menu: None,
quick_switcher: None,
page_title_cache: Vec::new(),
needs_linked_refs_refresh: false,
placeholder_uids: HashSet::new(),
}
}
pub fn can_nav_back(&self) -> bool {
if self.nav_index == self.nav_history.len() {
!self.nav_history.is_empty()
} else {
self.nav_index > 0
}
}
pub fn can_nav_forward(&self) -> bool {
self.nav_index + 1 < self.nav_history.len()
}
pub fn flat_block_count(&self) -> usize {
self.days
.iter()
.map(|d| count_blocks_recursive(&d.blocks))
.sum()
}
pub fn total_navigable_count(&self) -> usize {
let mut total = 0;
for day in &self.days {
total += count_blocks_recursive(&day.blocks);
if let Some(lr) = self.linked_refs.get(&day.title) {
total += linked_ref_section_count(lr);
}
}
total
}
pub fn resolve_linked_ref_item(&self, index: usize) -> Option<LinkedRefItem> {
let mut pos = 0;
for day in &self.days {
let block_count = count_blocks_recursive(&day.blocks);
if index < pos + block_count {
return None; }
pos += block_count;
if let Some(lr) = self.linked_refs.get(&day.title) {
let lr_count = linked_ref_section_count(lr);
if lr_count > 0 && index < pos + lr_count {
return resolve_within_linked_refs(lr, index - pos);
}
pos += lr_count;
}
}
None
}
pub fn linked_ref_day_at(&self, index: usize) -> Option<String> {
let mut pos = 0;
for day in &self.days {
pos += count_blocks_recursive(&day.blocks);
if let Some(lr) = self.linked_refs.get(&day.title) {
let lr_count = linked_ref_section_count(lr);
if lr_count > 0 && index < pos + lr_count {
return Some(day.title.clone());
}
pos += lr_count;
}
}
None
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Selection {
Single(usize),
Range { anchor: usize, head: usize },
}
impl Selection {
#[allow(dead_code)]
pub fn cursor(&self) -> usize {
match self {
Self::Single(i) => *i,
Self::Range { head, .. } => *head,
}
}
pub fn contains(&self, index: usize) -> bool {
match self {
Self::Single(i) => *i == index,
Self::Range { anchor, head } => {
let (lo, hi) = if anchor <= head {
(*anchor, *head)
} else {
(*head, *anchor)
};
index >= lo && index <= hi
}
}
}
pub fn indices(&self) -> (usize, usize) {
match self {
Self::Single(i) => (*i, *i),
Self::Range { anchor, head } => {
if anchor <= head {
(*anchor, *head)
} else {
(*head, *anchor)
}
}
}
}
#[allow(dead_code)]
pub fn count(&self) -> usize {
let (lo, hi) = self.indices();
hi - lo + 1
}
pub fn is_multi(&self) -> bool {
matches!(self, Self::Range { anchor, head } if anchor != head)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InputMode {
Normal,
Insert {
buffer: EditBuffer,
block_uid: String,
original_text: String,
create_info: Option<CreateInfo>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct CreateInfo {
pub parent_uid: String,
pub order: i64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BlockInfo {
pub block_uid: String,
pub parent_uid: String,
pub text: String,
pub order: i64,
pub depth: usize,
}
pub(crate) fn linked_ref_section_count(lr: &LinkedRefsState) -> usize {
if lr.groups.is_empty() {
return 0;
}
if lr.collapsed {
1 } else {
1 + lr.groups.iter().map(|g| 1 + g.blocks.len()).sum::<usize>()
}
}
fn resolve_within_linked_refs(lr: &LinkedRefsState, offset: usize) -> Option<LinkedRefItem> {
if lr.groups.is_empty() {
return None;
}
if offset == 0 {
return Some(LinkedRefItem::SectionHeader);
}
if lr.collapsed {
return None;
}
let mut pos = 1; for group in &lr.groups {
if offset == pos {
return Some(LinkedRefItem::GroupHeader(group.page_title.clone()));
}
pos += 1;
for block in &group.blocks {
if offset == pos {
return Some(LinkedRefItem::Block(block.clone()));
}
pos += 1;
}
}
None
}
pub(crate) fn count_blocks_recursive(blocks: &[Block]) -> usize {
blocks
.iter()
.map(|b| {
if b.open {
1 + count_blocks_recursive(&b.children)
} else {
1
}
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selection_single_contains_only_itself() {
let sel = Selection::Single(3);
assert!(sel.contains(3));
assert!(!sel.contains(2));
assert!(!sel.contains(4));
}
#[test]
fn selection_range_contains_all_in_range() {
let sel = Selection::Range { anchor: 2, head: 5 };
assert!(!sel.contains(1));
assert!(sel.contains(2));
assert!(sel.contains(3));
assert!(sel.contains(4));
assert!(sel.contains(5));
assert!(!sel.contains(6));
}
#[test]
fn selection_range_reversed_contains_all() {
let sel = Selection::Range { anchor: 5, head: 2 };
assert!(sel.contains(2));
assert!(sel.contains(3));
assert!(sel.contains(5));
assert!(!sel.contains(1));
assert!(!sel.contains(6));
}
#[test]
fn selection_indices_sorted() {
let sel = Selection::Range { anchor: 7, head: 3 };
assert_eq!(sel.indices(), (3, 7));
}
#[test]
fn selection_single_indices() {
let sel = Selection::Single(4);
assert_eq!(sel.indices(), (4, 4));
}
#[test]
fn selection_is_multi() {
assert!(!Selection::Single(0).is_multi());
assert!(!Selection::Range { anchor: 3, head: 3 }.is_multi());
assert!(Selection::Range { anchor: 1, head: 3 }.is_multi());
}
#[test]
fn selection_count() {
assert_eq!(Selection::Single(0).count(), 1);
assert_eq!(Selection::Range { anchor: 2, head: 5 }.count(), 4);
}
#[test]
fn selection_cursor() {
assert_eq!(Selection::Single(3).cursor(), 3);
assert_eq!(Selection::Range { anchor: 1, head: 5 }.cursor(), 5);
}
}