//! Text objects: the word, the quoted text or the bracketed text the cursor //! is in, for `diw`, `ca"` or `yi(`. use std::ops::Range; use super::motion::{Class, class_at, line_end, line_start, next, previous}; use crate::input::command::TextObject; /// What a text object covers at `at` (with its edges when `around`), or /// `None` when the cursor isn't in one. pub fn range(text: &str, at: usize, object: TextObject, around: bool) -> Option> { match object { TextObject::Word => word(text, at, around), TextObject::Quotes => quotes(text, at, around), TextObject::Parens => parens(text, at, around), } } /// `iw`: the word (or the run of spaces) the cursor is on. `aw`: the word /// and the spaces after it, or else the ones before it. fn word(text: &str, at: usize, around: bool) -> Option> { let (start, end) = (line_start(text, at), line_end(text, at)); let class = class_at(text, at).filter(|_| at < end)?; let run = |from: usize, class: Class| { let mut to = from; while to < end && class_at(text, to) == Some(class) { to = next(text, to); } to }; let mut first = at; while first > start && class_at(text, previous(text, first)) == Some(class) { first = previous(text, first); } let last = run(at, class); if !around { return Some(first..last); } if class == Class::Blank { // On spaces, `aw` takes them and the word after. return Some(first..class_at(text, last).map_or(last, |after| run(last, after))); } let spaces_after = run(last, Class::Blank); if spaces_after > last { return Some(first..spaces_after); } let mut spaces_before = first; while spaces_before > start && class_at(text, previous(text, spaces_before)) == Some(Class::Blank) { spaces_before = previous(text, spaces_before); } Some(spaces_before..last) } /// `i"`: the text between the quotes the cursor is in or on, on its line; /// the first quoted text after the cursor when it's in none. `a"`: the /// quotes too, and the spaces after them (or else before), as in vim. fn quotes(text: &str, at: usize, around: bool) -> Option> { let (start, end) = (line_start(text, at), line_end(text, at)); let quotes: Vec = text[start..end].match_indices('"').map(|(found, _)| start + found).collect(); let (open, close) = match quotes.iter().position(|"e| quote == at) { Some(index) if index % 2 == 0 => (index, index + 1), Some(index) => (index - 1, index), None => { let before = quotes.iter().filter(|&"e| quote < at).count(); if before % 2 == 1 { (before - 1, before) } else { (before, before + 1) } } }; let (open, close) = (*quotes.get(open)?, *quotes.get(close)?); if !around { return Some(open + 1..close); } // Quotes and spaces are one byte each. let after = close + 1 + text[close + 1..end].len() - text[close + 1..end].trim_start_matches(' ').len(); let before = if after > close + 1 { open } else { start + text[start..open].trim_end_matches(' ').len() }; Some(before..after) } /// `i(`: what's between the brackets around the cursor (they can be on /// other lines). `a(`: the brackets too. fn parens(text: &str, at: usize, around: bool) -> Option> { let open = if text[at..].starts_with('(') { at } else { let mut depth = 0; let mut found = None; for (index, c) in text[..at].char_indices().rev() { match c { ')' => depth += 1, '(' if depth == 0 => { found = Some(index); break; } '(' => depth -= 1, _ => {} } } found? }; let mut depth = 0; let mut close = None; for (index, c) in text[open + 1..].char_indices() { match c { '(' => depth += 1, ')' if depth == 0 => { close = Some(open + 1 + index); break; } ')' => depth -= 1, _ => {} } } let close = close?; Some(if around { open..close + 1 } else { open + 1..close }) } #[cfg(test)] mod tests { use super::*; /// The text an object covers, the cursor at '|' in `marked`. fn covered(marked: &str, object: TextObject, around: bool) -> Option { let at = marked.find('|').unwrap(); let text = marked.replacen('|', "", 1); range(&text, at, object, around).map(|range| text[range].to_string()) } #[test] fn words() { assert_eq!(covered("raised b|eds here", TextObject::Word, false).unwrap(), "beds"); assert_eq!(covered("raised b|eds here", TextObject::Word, true).unwrap(), "beds "); assert_eq!(covered("raised b|eds", TextObject::Word, true).unwrap(), " beds", "no space after"); assert_eq!(covered("raised | beds", TextObject::Word, true).unwrap(), " beds", "on spaces"); assert_eq!(covered("|", TextObject::Word, false), None, "nothing on an empty line"); } #[test] fn quotes_and_brackets() { let quoted = "say \"hi there\" now"; assert_eq!(covered("say \"hi th|ere\" now", TextObject::Quotes, false).unwrap(), "hi there"); assert_eq!(covered("say \"hi th|ere\" now", TextObject::Quotes, true).unwrap(), "\"hi there\" "); assert_eq!( covered("s|ay \"hi there\" now", TextObject::Quotes, false).unwrap(), "hi there", "the next pair" ); assert_eq!(covered(&format!("{quoted}|"), TextObject::Quotes, false), None); assert_eq!(covered("f(a, (b|), c)", TextObject::Parens, false).unwrap(), "b"); assert_eq!(covered("f(a, (b), |c)", TextObject::Parens, true).unwrap(), "(a, (b), c)"); assert_eq!(covered("f(a,\n|b)", TextObject::Parens, false).unwrap(), "a,\nb", "across lines"); assert_eq!(covered("no |brackets", TextObject::Parens, false), None); } }