use std::{iter::Peekable, ops::Range, slice::Iter};
use ratatui::widgets::ListState;
use crate::note_editor::markdown_parser::{HeadingLevel, MarkdownNode, Node};
use super::item::{FindItem, Flatten, Item};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct OutlineState {
pub(crate) selected_item_index: Option<usize>,
pub(crate) max_heading_count: usize,
pub(crate) items: Vec<Item>,
pub(crate) open: bool,
pub(crate) list_state: ListState,
pub(crate) active: bool,
}
#[derive(Debug, Clone, PartialEq)]
struct Heading {
index: usize,
level: HeadingLevel,
content: String,
}
#[derive(Debug, Clone, PartialEq)]
struct HeadingEntry {
range: Range<usize>,
level: HeadingLevel,
content: String,
children: Vec<HeadingEntry>,
}
impl From<HeadingEntry> for Item {
fn from(value: HeadingEntry) -> Self {
if value.children.is_empty() {
Item::Heading {
range: value.range,
content: value.content,
}
} else {
Item::HeadingEntry {
range: value.range,
content: value.content,
children: value.children.into_iter().map(Item::from).collect(),
expanded: false,
}
}
}
}
fn build_outline_tree(headings: &[Heading], max_end: usize) -> Vec<HeadingEntry> {
fn build_outline_tree_rec(
headings: &mut Peekable<Iter<Heading>>,
parent_level: Option<HeadingLevel>,
max_end: usize,
) -> Vec<HeadingEntry> {
let mut result: Vec<HeadingEntry> = vec![];
while let Some(next_heading) = headings.peek() {
if parent_level.is_some_and(|parent_level| next_heading.level <= parent_level) {
break;
}
if let Some(heading) = headings.next() {
let next_heading = headings.peek();
let range_start = heading.index;
let range_end = next_heading
.map(|next_heading| next_heading.index)
.unwrap_or(max_end);
let children = match next_heading {
Some(next_heading) if next_heading.level > heading.level => {
build_outline_tree_rec(headings, Some(heading.level), max_end)
}
_ => vec![],
};
result.push(HeadingEntry {
range: range_start..range_end,
level: heading.level,
content: heading.content.clone(),
children,
});
}
}
result
}
build_outline_tree_rec(&mut headings.iter().peekable(), None, max_end)
}
trait NodesAsHeadings {
fn to_headings(&self) -> Vec<Heading>;
}
impl NodesAsHeadings for &[Node] {
fn to_headings(&self) -> Vec<Heading> {
self.iter()
.enumerate()
.filter_map(|(index, node)| {
if let MarkdownNode::Heading { level, text } = &node.markdown_node {
Some(Heading {
index,
level: *level,
content: text.into(),
})
} else {
None
}
})
.collect()
}
}
trait HeadingsAsItems {
fn to_items(&self, max_end: usize) -> Vec<Item>;
}
impl HeadingsAsItems for Vec<Heading> {
fn to_items(&self, max_end: usize) -> Vec<Item> {
build_outline_tree(self, max_end)
.into_iter()
.map(Item::from)
.collect()
}
}
impl OutlineState {
pub fn new(nodes: &[Node], index: usize, open: bool) -> Self {
let headings = nodes.to_headings();
let max_heading_count = headings.len();
let mut state = OutlineState {
open,
max_heading_count,
selected_item_index: None,
items: headings.to_items(nodes.len()),
list_state: ListState::default(),
..Default::default()
};
state.select_at(index);
state.expand_all();
state
}
pub fn set_nodes(&mut self, nodes: &[Node]) {
let headings = nodes.to_headings();
let max_heading_count = headings.len();
self.max_heading_count = max_heading_count;
self.items = headings.to_items(nodes.len());
self.expand_all();
}
pub fn selected(&self) -> Option<Item> {
if let Some(selected) = self.list_state.selected() {
self.items.flatten().get(selected).cloned()
} else {
None
}
}
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
pub fn toggle(&mut self) {
self.open = !self.open;
}
pub fn open(&mut self) {
self.open = true;
}
pub fn close(&mut self) {
self.open = false;
}
fn toggle_item_in_tree(item: &Item, target_range: &Range<usize>, should_toggle: bool) -> Item {
let item = item.clone();
match item {
Item::HeadingEntry {
range: heading_range,
content,
expanded,
children,
} => {
let expanded = if heading_range == *target_range && should_toggle {
!expanded
} else {
expanded
};
Item::HeadingEntry {
range: heading_range.clone(),
content,
expanded,
children: children
.iter()
.map(|item| Self::toggle_item_in_tree(item, target_range, should_toggle))
.collect(),
}
}
_ => item,
}
}
pub fn toggle_item(&mut self) {
let index = self.list_state.selected().unwrap_or_default();
let items = self.items.flatten();
let selected_item = items.get(index);
if let Some(Item::HeadingEntry { range, .. }) = selected_item {
let target_range = range.clone();
self.items = self
.items
.iter()
.map(|item| Self::toggle_item_in_tree(item, &target_range, true))
.collect();
};
}
pub fn select_at(&mut self, index: usize) {
let (selected_item_index, _) = self.items.find_item(index).unzip();
self.selected_item_index = selected_item_index;
self.list_state.select(selected_item_index);
}
fn expanded_to_all_items(items: &[Item], expanded: bool) -> Vec<Item> {
items
.iter()
.map(|item| match item {
Item::HeadingEntry {
range,
content,
children,
..
} => Item::HeadingEntry {
range: range.clone(),
content: content.clone(),
children: Self::expanded_to_all_items(children, expanded),
expanded,
},
heading => heading.clone(),
})
.collect()
}
pub fn expand_all(&mut self) {
self.items = Self::expanded_to_all_items(self.items.as_slice(), true);
}
pub fn collapse_all(&mut self) {
self.items = Self::expanded_to_all_items(self.items.as_slice(), false);
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn next(&mut self, amount: usize) {
let index = self
.list_state
.selected()
.map(|i| (i + amount).min(self.max_heading_count.saturating_sub(1)))
.unwrap_or_default();
self.list_state.select(Some(index));
}
pub fn previous(&mut self, amount: usize) {
let index = self.list_state.selected().map(|i| i.saturating_sub(amount));
self.list_state.select(index);
}
}