use serde::{Deserialize, Serialize};
use crate::index::MarkdownIndex;
use crate::range::ByteRange;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectionMoveResult {
pub text: String,
pub new_heading_offset: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
pub enum SectionError {
#[error("heading index {0} out of range")]
HeadingIndexOutOfRange(usize),
#[error("no sibling section exists in that direction")]
NoSibling,
#[error("section boundaries could not be resolved")]
BoundaryError,
}
pub fn section_range(text: &str, index: &MarkdownIndex, heading_idx: usize) -> Option<ByteRange> {
let headings = &index.headings;
if heading_idx >= headings.len() {
return None;
}
let start = headings[heading_idx].source_range.start;
let level = headings[heading_idx].level;
let end = headings[heading_idx + 1..]
.iter()
.find(|h| h.level <= level)
.map(|h| h.source_range.start)
.unwrap_or(text.len());
Some(ByteRange::new(start, end))
}
pub fn move_section_up(
text: &str,
index: &MarkdownIndex,
heading_idx: usize,
) -> Result<SectionMoveResult, SectionError> {
if heading_idx >= index.headings.len() {
return Err(SectionError::HeadingIndexOutOfRange(heading_idx));
}
let level = index.headings[heading_idx].level;
let prev_idx = (0..heading_idx)
.rev()
.find(|&i| index.headings[i].level == level)
.ok_or(SectionError::NoSibling)?;
let has_parent_between = (prev_idx + 1..heading_idx).any(|i| index.headings[i].level < level);
if has_parent_between {
return Err(SectionError::NoSibling);
}
let range_prev = section_range(text, index, prev_idx).ok_or(SectionError::BoundaryError)?;
let range_curr = section_range(text, index, heading_idx).ok_or(SectionError::BoundaryError)?;
swap_sections(text, range_prev, range_curr)
}
pub fn move_section_down(
text: &str,
index: &MarkdownIndex,
heading_idx: usize,
) -> Result<SectionMoveResult, SectionError> {
if heading_idx >= index.headings.len() {
return Err(SectionError::HeadingIndexOutOfRange(heading_idx));
}
let level = index.headings[heading_idx].level;
let next_idx = (heading_idx + 1..index.headings.len())
.find(|&i| index.headings[i].level == level)
.ok_or(SectionError::NoSibling)?;
let has_parent_between = (heading_idx + 1..next_idx).any(|i| index.headings[i].level < level);
if has_parent_between {
return Err(SectionError::NoSibling);
}
let range_curr = section_range(text, index, heading_idx).ok_or(SectionError::BoundaryError)?;
let range_next = section_range(text, index, next_idx).ok_or(SectionError::BoundaryError)?;
let result = swap_sections(text, range_curr, range_next)?;
Ok(result)
}
fn swap_sections(
text: &str,
first: ByteRange,
second: ByteRange,
) -> Result<SectionMoveResult, SectionError> {
let first_text = &text[first.start..first.end];
let second_text = &text[second.start..second.end];
let gap = &text[first.end..second.start];
let mut result = String::with_capacity(text.len());
result.push_str(&text[..first.start]);
result.push_str(second_text);
result.push_str(gap);
result.push_str(first_text);
result.push_str(&text[second.end..]);
let new_heading_offset = first.start;
Ok(SectionMoveResult {
text: result,
new_heading_offset,
})
}