#[derive(Clone, Copy)]
pub enum LineMove {
Up,
Down,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Segment {
Line,
Block,
Section,
}
#[must_use]
pub fn move_line(text: &str, cursor: usize, direction: LineMove) -> Option<(String, usize)> {
move_segment(text, cursor, Segment::Line, direction)
}
#[must_use]
pub fn move_segment(
text: &str,
cursor: usize,
segment: Segment,
direction: LineMove,
) -> Option<(String, usize)> {
let lines: Vec<&str> = text.split('\n').collect();
let (line, column) = line_column(text, cursor);
let (start, end) = segment_range(&lines, line, segment)?;
let (other_start, other_end) = neighbour_range(&lines, start, end, segment, direction)?;
let moving_first = start < other_start;
let (first, second) = if moving_first {
((start, end), (other_start, other_end))
} else {
((other_start, other_end), (start, end))
};
let mut swapped: Vec<&str> = Vec::with_capacity(lines.len());
swapped.extend_from_slice(lines.get(..first.0)?);
swapped.extend_from_slice(lines.get(second.0..second.1)?);
swapped.extend_from_slice(lines.get(first.1..second.0)?);
swapped.extend_from_slice(lines.get(first.0..first.1)?);
swapped.extend_from_slice(lines.get(second.1..)?);
let moved_start = if moving_first {
first.0 + (second.1 - second.0) + (second.0 - first.1)
} else {
first.0
};
let target_line = moved_start + (line - start);
let target_column = column.min(swapped.get(target_line)?.chars().count());
let moved_cursor = cursor_at(&swapped, target_line, target_column);
Some((swapped.join("\n"), moved_cursor))
}
fn segment_range(lines: &[&str], line: usize, segment: Segment) -> Option<(usize, usize)> {
match segment {
Segment::Line => Some((line, line + 1)),
Segment::Block => {
if lines.get(line)?.trim().is_empty() {
return None;
}
let start = line - filled_run(lines.iter().take(line).rev());
let end = line + 1 + filled_run(lines.iter().skip(line + 1));
Some((start, end))
}
Segment::Section => {
let start = lines
.iter()
.take(line + 1)
.rposition(|text| heading_level(text).is_some())?;
let level = heading_level(lines.get(start)?)?;
Some((start, section_end(lines, start, level)))
}
}
}
fn neighbour_range(
lines: &[&str],
start: usize,
end: usize,
segment: Segment,
direction: LineMove,
) -> Option<(usize, usize)> {
match (segment, direction) {
(Segment::Line, LineMove::Up) => Some((start.checked_sub(1)?, start)),
(Segment::Line, LineMove::Down) => (end < lines.len()).then_some((end, end + 1)),
(Segment::Block, LineMove::Up) => {
let other_end = start - blank_run(lines.iter().take(start).rev());
let last = other_end.checked_sub(1)?;
let other_start = last - filled_run(lines.iter().take(last).rev());
Some((other_start, other_end))
}
(Segment::Block, LineMove::Down) => {
let other_start = end + blank_run(lines.iter().skip(end));
if other_start >= lines.len() {
return None;
}
let other_end = other_start + 1 + filled_run(lines.iter().skip(other_start + 1));
Some((other_start, other_end))
}
(Segment::Section, LineMove::Up) => {
let level = heading_level(lines.get(start)?)?;
let other_start = lines
.iter()
.take(start)
.rposition(|text| heading_level(text).is_some())?;
(heading_level(lines.get(other_start)?) == Some(level)).then_some((other_start, start))
}
(Segment::Section, LineMove::Down) => {
let level = heading_level(lines.get(start)?)?;
(heading_level(lines.get(end)?) == Some(level))
.then(|| (end, section_end(lines, end, level)))
}
}
}
fn blank_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
lines.take_while(|text| text.trim().is_empty()).count()
}
fn filled_run<'a>(lines: impl Iterator<Item = &'a &'a str>) -> usize {
lines.take_while(|text| !text.trim().is_empty()).count()
}
fn section_end(lines: &[&str], start: usize, level: usize) -> usize {
lines
.iter()
.enumerate()
.skip(start + 1)
.find(|(_, text)| heading_level(text).is_some_and(|found| found <= level))
.map_or(lines.len(), |(index, _)| index)
}
fn heading_level(line: &str) -> Option<usize> {
let hashes = line.chars().take_while(|&c| c == '#').count();
(hashes > 0 && line.chars().nth(hashes) == Some(' ')).then_some(hashes)
}
#[must_use]
pub fn line_column(text: &str, cursor: usize) -> (usize, usize) {
let mut line = 0;
let mut column = 0;
for (index, character) in text.chars().enumerate() {
if index == cursor {
break;
}
if character == '\n' {
line += 1;
column = 0;
} else {
column += 1;
}
}
(line, column)
}
#[must_use]
pub fn cursor_at(lines: &[&str], line: usize, column: usize) -> usize {
let preceding: usize = lines
.iter()
.take(line)
.map(|text| text.chars().count() + 1)
.sum();
preceding + column
}