bekoedit_markdown/
sections.rs1use serde::{Deserialize, Serialize};
9
10use crate::index::MarkdownIndex;
11use crate::range::ByteRange;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SectionMoveResult {
16 pub text: String,
18 pub new_heading_offset: usize,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
24pub enum SectionError {
25 #[error("heading index {0} out of range")]
26 HeadingIndexOutOfRange(usize),
27 #[error("no sibling section exists in that direction")]
28 NoSibling,
29 #[error("section boundaries could not be resolved")]
30 BoundaryError,
31}
32
33pub fn section_range(text: &str, index: &MarkdownIndex, heading_idx: usize) -> Option<ByteRange> {
37 let headings = &index.headings;
38 if heading_idx >= headings.len() {
39 return None;
40 }
41 let start = headings[heading_idx].source_range.start;
42 let level = headings[heading_idx].level;
43
44 let end = headings[heading_idx + 1..]
46 .iter()
47 .find(|h| h.level <= level)
48 .map(|h| h.source_range.start)
49 .unwrap_or(text.len());
50
51 Some(ByteRange::new(start, end))
52}
53
54pub fn move_section_up(
57 text: &str,
58 index: &MarkdownIndex,
59 heading_idx: usize,
60) -> Result<SectionMoveResult, SectionError> {
61 if heading_idx >= index.headings.len() {
62 return Err(SectionError::HeadingIndexOutOfRange(heading_idx));
63 }
64 let level = index.headings[heading_idx].level;
65
66 let prev_idx = (0..heading_idx)
68 .rev()
69 .find(|&i| index.headings[i].level == level)
70 .ok_or(SectionError::NoSibling)?;
71
72 let has_parent_between = (prev_idx + 1..heading_idx).any(|i| index.headings[i].level < level);
74 if has_parent_between {
75 return Err(SectionError::NoSibling);
76 }
77
78 let range_prev = section_range(text, index, prev_idx).ok_or(SectionError::BoundaryError)?;
79 let range_curr = section_range(text, index, heading_idx).ok_or(SectionError::BoundaryError)?;
80
81 swap_sections(text, range_prev, range_curr)
82}
83
84pub fn move_section_down(
86 text: &str,
87 index: &MarkdownIndex,
88 heading_idx: usize,
89) -> Result<SectionMoveResult, SectionError> {
90 if heading_idx >= index.headings.len() {
91 return Err(SectionError::HeadingIndexOutOfRange(heading_idx));
92 }
93 let level = index.headings[heading_idx].level;
94
95 let next_idx = (heading_idx + 1..index.headings.len())
97 .find(|&i| index.headings[i].level == level)
98 .ok_or(SectionError::NoSibling)?;
99
100 let has_parent_between = (heading_idx + 1..next_idx).any(|i| index.headings[i].level < level);
101 if has_parent_between {
102 return Err(SectionError::NoSibling);
103 }
104
105 let range_curr = section_range(text, index, heading_idx).ok_or(SectionError::BoundaryError)?;
106 let range_next = section_range(text, index, next_idx).ok_or(SectionError::BoundaryError)?;
107
108 let result = swap_sections(text, range_curr, range_next)?;
109 Ok(result)
111}
112
113fn swap_sections(
114 text: &str,
115 first: ByteRange,
116 second: ByteRange,
117) -> Result<SectionMoveResult, SectionError> {
118 let first_text = &text[first.start..first.end];
123 let second_text = &text[second.start..second.end];
124 let gap = &text[first.end..second.start];
125
126 let mut result = String::with_capacity(text.len());
127 result.push_str(&text[..first.start]);
128 result.push_str(second_text);
129 result.push_str(gap);
130 result.push_str(first_text);
131 result.push_str(&text[second.end..]);
132
133 let new_heading_offset = first.start;
134 Ok(SectionMoveResult {
135 text: result,
136 new_heading_offset,
137 })
138}