1#[derive(Clone, Copy)]
5pub enum LineMove {
6 Up,
8 Down,
10}
11
12#[derive(Clone, Copy, PartialEq, Eq)]
14pub enum Segment {
15 Line,
17 Block,
19 Section,
21}
22
23#[must_use]
28pub fn move_line(text: &str, cursor: usize, direction: LineMove) -> Option<(String, usize)> {
29 move_segment(text, cursor, Segment::Line, direction)
30}
31
32#[must_use]
39pub fn move_segment(
40 text: &str,
41 cursor: usize,
42 segment: Segment,
43 direction: LineMove,
44) -> Option<(String, usize)> {
45 let lines: Vec<&str> = text.split('\n').collect();
46 let (line, column) = line_column(text, cursor);
47 let (start, end) = segment_range(&lines, line, segment)?;
48 let (other_start, other_end) = neighbour_range(&lines, start, end, segment, direction)?;
49 let moving_first = start < other_start;
50 let (first, second) = if moving_first {
51 ((start, end), (other_start, other_end))
52 } else {
53 ((other_start, other_end), (start, end))
54 };
55 let mut swapped: Vec<&str> = Vec::with_capacity(lines.len());
56 swapped.extend_from_slice(lines.get(..first.0)?);
57 swapped.extend_from_slice(lines.get(second.0..second.1)?);
58 swapped.extend_from_slice(lines.get(first.1..second.0)?);
59 swapped.extend_from_slice(lines.get(first.0..first.1)?);
60 swapped.extend_from_slice(lines.get(second.1..)?);
61 let moved_start = if moving_first {
62 first.0 + (second.1 - second.0) + (second.0 - first.1)
63 } else {
64 first.0
65 };
66 let target_line = moved_start + (line - start);
67 let target_column = column.min(swapped.get(target_line)?.chars().count());
68 let moved_cursor = cursor_at(&swapped, target_line, target_column);
69 Some((swapped.join("\n"), moved_cursor))
70}
71
72fn segment_range(lines: &[&str], line: usize, segment: Segment) -> Option<(usize, usize)> {
73 match segment {
74 Segment::Line => Some((line, line + 1)),
75 Segment::Block => {
76 if lines.get(line)?.trim().is_empty() {
77 return None;
78 }
79 let start = line - filled_run(lines.iter().take(line).rev());
80 let end = line + 1 + filled_run(lines.iter().skip(line + 1));
81 Some((start, end))
82 }
83 Segment::Section => {
84 let start = lines
85 .iter()
86 .take(line + 1)
87 .rposition(|text| heading_level(text).is_some())?;
88 let level = heading_level(lines.get(start)?)?;
89 Some((start, section_end(lines, start, level)))
90 }
91 }
92}
93
94fn neighbour_range(
95 lines: &[&str],
96 start: usize,
97 end: usize,
98 segment: Segment,
99 direction: LineMove,
100) -> Option<(usize, usize)> {
101 match (segment, direction) {
102 (Segment::Line, LineMove::Up) => Some((start.checked_sub(1)?, start)),
103 (Segment::Line, LineMove::Down) => (end < lines.len()).then_some((end, end + 1)),
104 (Segment::Block, LineMove::Up) => {
105 let other_end = start - blank_run(lines.iter().take(start).rev());
106 let last = other_end.checked_sub(1)?;
107 let other_start = last - filled_run(lines.iter().take(last).rev());
108 Some((other_start, other_end))
109 }
110 (Segment::Block, LineMove::Down) => {
111 let other_start = end + blank_run(lines.iter().skip(end));
112 if other_start >= lines.len() {
113 return None;
114 }
115 let other_end = other_start + 1 + filled_run(lines.iter().skip(other_start + 1));
116 Some((other_start, other_end))
117 }
118 (Segment::Section, LineMove::Up) => {
119 let level = heading_level(lines.get(start)?)?;
120 let other_start = lines
121 .iter()
122 .take(start)
123 .rposition(|text| heading_level(text).is_some())?;
124 (heading_level(lines.get(other_start)?) == Some(level)).then_some((other_start, start))
125 }
126 (Segment::Section, LineMove::Down) => {
127 let level = heading_level(lines.get(start)?)?;
128 (heading_level(lines.get(end)?) == Some(level))
129 .then(|| (end, section_end(lines, end, level)))
130 }
131 }
132}
133
134fn blank_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
135 lines.take_while(|text| text.trim().is_empty()).count()
136}
137
138fn filled_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
139 lines.take_while(|text| !text.trim().is_empty()).count()
140}
141
142fn section_end(lines: &[&str], start: usize, level: usize) -> usize {
143 lines
144 .iter()
145 .enumerate()
146 .skip(start + 1)
147 .find(|(_, text)| heading_level(text).is_some_and(|found| found <= level))
148 .map_or(lines.len(), |(index, _)| index)
149}
150
151fn heading_level(line: &str) -> Option<usize> {
152 let hashes = line.chars().take_while(|&c| c == '#').count();
153 (hashes > 0 && line.chars().nth(hashes) == Some(' ')).then_some(hashes)
154}
155
156#[must_use]
158pub fn line_column(text: &str, cursor: usize) -> (usize, usize) {
159 let mut line = 0;
160 let mut column = 0;
161 for (index, character) in text.chars().enumerate() {
162 if index == cursor {
163 break;
164 }
165 if character == '\n' {
166 line += 1;
167 column = 0;
168 } else {
169 column += 1;
170 }
171 }
172 (line, column)
173}
174
175#[must_use]
178pub fn cursor_at(lines: &[&str], line: usize, column: usize) -> usize {
179 let preceding: usize = lines
180 .iter()
181 .take(line)
182 .map(|text| text.chars().count() + 1)
183 .sum();
184 preceding + column
185}