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 = neighbour_range(&lines, start, end, segment, direction)?;
49 let (swapped, moved_start) = swap_ranges(&lines, (start, end), other)?;
50 let target_line = moved_start + (line - start);
51 let target_column = column.min(swapped.get(target_line)?.chars().count());
52 let moved_cursor = cursor_at(&swapped, target_line, target_column);
53 Some((swapped.join("\n"), moved_cursor))
54}
55
56#[must_use]
65pub fn move_segments(
66 text: &str,
67 selection: (usize, usize),
68 segment: Segment,
69 direction: LineMove,
70) -> Option<(String, (usize, usize))> {
71 let lines: Vec<&str> = text.split('\n').collect();
72 let (from, to) = (selection.0.min(selection.1), selection.0.max(selection.1));
73 let (first_line, first_column) = line_column(text, from);
74 let (last_line, last_column) = line_column(text, to);
75 let last = if last_column == 0 && last_line > first_line {
76 last_line - 1
77 } else {
78 last_line
79 };
80 let (start, end) = selection_range(&lines, first_line, last, segment)?;
81 let other = neighbour_range(&lines, start, end, segment, direction)?;
82 let (swapped, moved_start) = swap_ranges(&lines, (start, end), other)?;
83 let moved = swapped.join("\n");
84 let moved_from = cursor_in(
85 &moved,
86 first_line.max(start) + moved_start - start,
87 first_column,
88 );
89 let moved_to = cursor_in(
90 &moved,
91 last_line.min(end) + moved_start - start,
92 last_column,
93 );
94 Some((moved, (moved_from, moved_to)))
95}
96
97fn selection_range(
98 lines: &[&str],
99 first: usize,
100 last: usize,
101 segment: Segment,
102) -> Option<(usize, usize)> {
103 let (first, last) = if segment == Segment::Block {
104 let filled = |line: &usize| lines.get(*line).is_some_and(|text| !text.trim().is_empty());
105 (
106 (first..=last).find(filled)?,
107 (first..=last).rev().find(filled)?,
108 )
109 } else {
110 (first, last)
111 };
112 let (start, first_end) = segment_range(lines, first, segment)?;
113 let (last_start, end) = segment_range(lines, last, segment)?;
114 Some((start.min(last_start), end.max(first_end)))
115}
116
117fn swap_ranges<'a>(
118 lines: &[&'a str],
119 range: (usize, usize),
120 other: (usize, usize),
121) -> Option<(Vec<&'a str>, usize)> {
122 let moving_first = range.0 < other.0;
123 let (first, second) = if moving_first {
124 (range, other)
125 } else {
126 (other, range)
127 };
128 let mut swapped: Vec<&str> = Vec::with_capacity(lines.len());
129 swapped.extend_from_slice(lines.get(..first.0)?);
130 swapped.extend_from_slice(lines.get(second.0..second.1)?);
131 swapped.extend_from_slice(lines.get(first.1..second.0)?);
132 swapped.extend_from_slice(lines.get(first.0..first.1)?);
133 swapped.extend_from_slice(lines.get(second.1..)?);
134 let moved_start = if moving_first {
135 first.0 + (second.1 - second.0) + (second.0 - first.1)
136 } else {
137 first.0
138 };
139 Some((swapped, moved_start))
140}
141
142fn segment_range(lines: &[&str], line: usize, segment: Segment) -> Option<(usize, usize)> {
143 match segment {
144 Segment::Line => Some((line, line + 1)),
145 Segment::Block => {
146 if lines.get(line)?.trim().is_empty() {
147 return None;
148 }
149 let start = line - filled_run(lines.iter().take(line).rev());
150 let end = line + 1 + filled_run(lines.iter().skip(line + 1));
151 Some((start, end))
152 }
153 Segment::Section => {
154 let start = lines
155 .iter()
156 .take(line + 1)
157 .rposition(|text| heading_level(text).is_some())?;
158 let level = heading_level(lines.get(start)?)?;
159 Some((start, section_end(lines, start, level)))
160 }
161 }
162}
163
164fn neighbour_range(
165 lines: &[&str],
166 start: usize,
167 end: usize,
168 segment: Segment,
169 direction: LineMove,
170) -> Option<(usize, usize)> {
171 match (segment, direction) {
172 (Segment::Line, LineMove::Up) => Some((start.checked_sub(1)?, start)),
173 (Segment::Line, LineMove::Down) => (end < lines.len()).then_some((end, end + 1)),
174 (Segment::Block, LineMove::Up) => {
175 let other_end = start - blank_run(lines.iter().take(start).rev());
176 let last = other_end.checked_sub(1)?;
177 let other_start = last - filled_run(lines.iter().take(last).rev());
178 Some((other_start, other_end))
179 }
180 (Segment::Block, LineMove::Down) => {
181 let other_start = end + blank_run(lines.iter().skip(end));
182 if other_start >= lines.len() {
183 return None;
184 }
185 let other_end = other_start + 1 + filled_run(lines.iter().skip(other_start + 1));
186 Some((other_start, other_end))
187 }
188 (Segment::Section, LineMove::Up) => {
189 let level = heading_level(lines.get(start)?)?;
190 let other_start = lines
191 .iter()
192 .take(start)
193 .rposition(|text| heading_level(text).is_some())?;
194 (heading_level(lines.get(other_start)?) == Some(level)).then_some((other_start, start))
195 }
196 (Segment::Section, LineMove::Down) => {
197 let level = heading_level(lines.get(start)?)?;
198 (heading_level(lines.get(end)?) == Some(level))
199 .then(|| (end, section_end(lines, end, level)))
200 }
201 }
202}
203
204fn blank_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
205 lines.take_while(|text| text.trim().is_empty()).count()
206}
207
208fn filled_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
209 lines.take_while(|text| !text.trim().is_empty()).count()
210}
211
212fn section_end(lines: &[&str], start: usize, level: usize) -> usize {
213 lines
214 .iter()
215 .enumerate()
216 .skip(start + 1)
217 .find(|(_, text)| heading_level(text).is_some_and(|found| found <= level))
218 .map_or(lines.len(), |(index, _)| index)
219}
220
221fn heading_level(line: &str) -> Option<usize> {
222 let hashes = line.chars().take_while(|&c| c == '#').count();
223 (hashes > 0 && line.chars().nth(hashes) == Some(' ')).then_some(hashes)
224}
225
226#[must_use]
228pub fn line_column(text: &str, cursor: usize) -> (usize, usize) {
229 let mut line = 0;
230 let mut column = 0;
231 for (index, character) in text.chars().enumerate() {
232 if index == cursor {
233 break;
234 }
235 if character == '\n' {
236 line += 1;
237 column = 0;
238 } else {
239 column += 1;
240 }
241 }
242 (line, column)
243}
244
245#[must_use]
248pub fn cursor_in(text: &str, line: usize, column: usize) -> usize {
249 let lines: Vec<&str> = text.split('\n').collect();
250 let line = line.min(lines.len().saturating_sub(1));
251 let width = lines.get(line).map_or(0, |text| text.chars().count());
252 cursor_at(&lines, line, column.min(width))
253}
254
255#[must_use]
258pub fn cursor_at(lines: &[&str], line: usize, column: usize) -> usize {
259 let preceding: usize = lines
260 .iter()
261 .take(line)
262 .map(|text| text.chars().count() + 1)
263 .sum();
264 preceding + column
265}