use crate::text::buffer::Buffer;
use crate::text::cursor::Cursor;
use crate::text::object;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FocusMode {
Off,
Paragraph,
Sentence,
}
pub struct FocusRegion {
pub start: Cursor,
pub end: Cursor,
pub revision: u64,
}
impl FocusRegion {
pub fn compute(buffer: &Buffer, mode: FocusMode) -> Option<FocusRegion> {
if mode == FocusMode::Off {
return None;
}
let cursor = buffer.cursor;
let on_blank = buffer.line_text(cursor.line).trim().is_empty();
let (mut start, mut end) = if on_blank || mode == FocusMode::Paragraph {
let anchor = nearest_paragraph_line(buffer, cursor.line);
let (s, e) = object::paragraph_bounds(buffer, anchor);
(Cursor::new(s, 0), Cursor::new(e, buffer.line_len(e)))
} else {
let (s, e) = object::sentence_bounds(buffer, cursor);
(cursor_of(buffer, s), cursor_of(buffer, e))
};
if (cursor.line, cursor.col) < (start.line, start.col) {
start = Cursor::new(cursor.line, 0);
}
if (cursor.line, cursor.col) > (end.line, end.col) {
end = cursor;
}
Some(FocusRegion {
start,
end,
revision: buffer.revision,
})
}
pub fn still_valid(&self, buffer: &Buffer) -> bool {
self.revision == buffer.revision && self.contains(buffer.cursor)
}
pub fn contains(&self, c: Cursor) -> bool {
(c.line, c.col) >= (self.start.line, self.start.col)
&& (c.line, c.col) <= (self.end.line, self.end.col)
}
}
fn nearest_paragraph_line(buffer: &Buffer, line: usize) -> usize {
let blank = |l: usize| buffer.line_text(l).trim().is_empty();
if !blank(line) {
return line;
}
let last = buffer.line_count().saturating_sub(1);
if let Some(l) = (line + 1..=last).find(|&l| !blank(l)) {
return l;
}
(0..line).rev().find(|&l| !blank(l)).unwrap_or(line)
}
fn cursor_of(buffer: &Buffer, idx: usize) -> Cursor {
let idx = idx.min(buffer.rope.len_chars());
let line = buffer.rope.char_to_line(idx);
let col = idx - buffer.rope.line_to_char(line);
Cursor::new(line, col)
}
impl FocusMode {
pub fn next(self) -> Self {
match self {
FocusMode::Off => FocusMode::Paragraph,
FocusMode::Paragraph => FocusMode::Sentence,
FocusMode::Sentence => FocusMode::Off,
}
}
pub fn parse(s: &str) -> Option<FocusMode> {
Some(match s.trim() {
"off" | "none" => FocusMode::Off,
"paragraph" | "para" => FocusMode::Paragraph,
"sentence" | "sent" => FocusMode::Sentence,
_ => return None,
})
}
pub fn label(self) -> &'static str {
match self {
FocusMode::Off => "off",
FocusMode::Paragraph => "paragraph",
FocusMode::Sentence => "sentence",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::cursor::Cursor;
fn buf(text: &str, line: usize, col: usize) -> Buffer {
let mut b = Buffer::empty();
b.insert_str(Cursor::new(0, 0), text);
b.cursor = Cursor::new(line, col);
b
}
#[test]
fn paragraph_region_spans_the_block() {
let b = buf("a\nb\nc\n\nd\n", 1, 0); let r = FocusRegion::compute(&b, FocusMode::Paragraph).unwrap();
assert_eq!(r.start.line, 0);
assert_eq!(r.end.line, 2);
assert!(r.contains(Cursor::new(2, 0)));
assert!(!r.contains(Cursor::new(4, 0)));
}
#[test]
fn paragraph_region_survives_moves_within_it() {
let mut b = buf("one two\nthree\n\nfar\n", 0, 0);
let r = FocusRegion::compute(&b, FocusMode::Paragraph).unwrap();
b.cursor = Cursor::new(1, 2); assert!(r.still_valid(&b));
b.cursor = Cursor::new(3, 0); assert!(!r.still_valid(&b));
}
#[test]
fn blank_line_attaches_to_the_next_paragraph_not_everything() {
let b = buf("# Title\n\nbody one\nbody two\n\nfar\n", 1, 0);
let r = FocusRegion::compute(&b, FocusMode::Paragraph).unwrap();
assert!(r.contains(Cursor::new(1, 0)), "cursor line stays bright");
assert!(r.contains(Cursor::new(2, 0)), "adjacent paragraph is bright");
assert!(r.contains(Cursor::new(3, 0)));
assert!(!r.contains(Cursor::new(5, 0)), "distant paragraph is dimmed");
}
#[test]
fn sentence_region_stops_at_terminators() {
let b = buf("Hello world. Second one.\n", 0, 3);
let r = FocusRegion::compute(&b, FocusMode::Sentence).unwrap();
assert_eq!(r.start.col, 0);
assert_eq!(r.end.col, 12); }
}