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[..first.0]);
57 swapped.extend_from_slice(&lines[second.0..second.1]);
58 swapped.extend_from_slice(&lines[first.1..second.0]);
59 swapped.extend_from_slice(&lines[first.0..first.1]);
60 swapped.extend_from_slice(&lines[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[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[line].trim().is_empty() {
77 return None;
78 }
79 let mut start = line;
80 while start > 0 && !lines[start - 1].trim().is_empty() {
81 start -= 1;
82 }
83 let mut end = line + 1;
84 while end < lines.len() && !lines[end].trim().is_empty() {
85 end += 1;
86 }
87 Some((start, end))
88 }
89 Segment::Section => {
90 let start = (0..=line)
91 .rev()
92 .find(|&index| heading_level(lines[index]).is_some())?;
93 let level = heading_level(lines[start])?;
94 Some((start, section_end(lines, start, level)))
95 }
96 }
97}
98
99fn neighbour_range(
100 lines: &[&str],
101 start: usize,
102 end: usize,
103 segment: Segment,
104 direction: LineMove,
105) -> Option<(usize, usize)> {
106 match (segment, direction) {
107 (Segment::Line, LineMove::Up) => Some((start.checked_sub(1)?, start)),
108 (Segment::Line, LineMove::Down) => (end < lines.len()).then_some((end, end + 1)),
109 (Segment::Block, LineMove::Up) => {
110 let mut other_end = start;
111 while other_end > 0 && lines[other_end - 1].trim().is_empty() {
112 other_end -= 1;
113 }
114 let mut other_start = other_end.checked_sub(1)?;
115 while other_start > 0 && !lines[other_start - 1].trim().is_empty() {
116 other_start -= 1;
117 }
118 Some((other_start, other_end))
119 }
120 (Segment::Block, LineMove::Down) => {
121 let mut other_start = end;
122 while other_start < lines.len() && lines[other_start].trim().is_empty() {
123 other_start += 1;
124 }
125 if other_start == lines.len() {
126 return None;
127 }
128 let mut other_end = other_start + 1;
129 while other_end < lines.len() && !lines[other_end].trim().is_empty() {
130 other_end += 1;
131 }
132 Some((other_start, other_end))
133 }
134 (Segment::Section, LineMove::Up) => {
135 let level = heading_level(lines[start])?;
136 let other_start = (0..start)
137 .rev()
138 .find(|&index| heading_level(lines[index]).is_some())?;
139 (heading_level(lines[other_start]) == Some(level)).then_some((other_start, start))
140 }
141 (Segment::Section, LineMove::Down) => {
142 let level = heading_level(lines[start])?;
143 (heading_level(lines.get(end)?) == Some(level))
144 .then(|| (end, section_end(lines, end, level)))
145 }
146 }
147}
148
149fn section_end(lines: &[&str], start: usize, level: usize) -> usize {
150 ((start + 1)..lines.len())
151 .find(|&index| heading_level(lines[index]).is_some_and(|found| found <= level))
152 .unwrap_or(lines.len())
153}
154
155fn heading_level(line: &str) -> Option<usize> {
156 let hashes = line.chars().take_while(|&c| c == '#').count();
157 (hashes > 0 && line.chars().nth(hashes) == Some(' ')).then_some(hashes)
158}
159
160#[must_use]
162pub fn line_column(text: &str, cursor: usize) -> (usize, usize) {
163 let mut line = 0;
164 let mut column = 0;
165 for (index, character) in text.chars().enumerate() {
166 if index == cursor {
167 break;
168 }
169 if character == '\n' {
170 line += 1;
171 column = 0;
172 } else {
173 column += 1;
174 }
175 }
176 (line, column)
177}
178
179#[must_use]
182pub fn cursor_at(lines: &[&str], line: usize, column: usize) -> usize {
183 let preceding: usize = lines[..line].iter().map(|l| l.chars().count() + 1).sum();
184 preceding + column
185}