use std::ops::Range;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum SelectionMode {
#[default]
Text,
CursorBefore,
CursorAfter,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TextSelection {
pub text: String,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub mode: SelectionMode,
}
impl TextSelection {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into(), ..Self::default() }
}
#[must_use]
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
#[must_use]
pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
self.suffix = Some(suffix.into());
self
}
#[must_use]
pub const fn mode(mut self, mode: SelectionMode) -> Self {
self.mode = mode;
self
}
pub fn resolve(&self, value: &str) -> Result<Range<usize>> {
if self.text.is_empty() {
return Err(Error::invalid("text to select must not be empty"));
}
let prefix = self.prefix.as_deref().unwrap_or("");
let suffix = self.suffix.as_deref().unwrap_or("");
let mut matches = value.match_indices(&self.text).filter_map(|(start, m)| {
let end = start + m.len();
(value[..start].ends_with(prefix) && value[end..].starts_with(suffix)).then_some(start..end)
});
let found = matches
.next()
.ok_or_else(|| Error::invalid(format!("text {:?} was not found in the element's value", self.text)))?;
if matches.next().is_some() {
return Err(Error::invalid(format!(
"text {:?} occurs more than once; add a prefix or suffix to disambiguate",
self.text
)));
}
Ok(match self.mode {
SelectionMode::Text => found,
SelectionMode::CursorBefore => found.start..found.start,
SelectionMode::CursorAfter => found.end..found.end,
})
}
}
#[must_use]
pub fn utf16_offset(value: &str, byte_offset: usize) -> usize {
value[..byte_offset].encode_utf16().count()
}
#[must_use]
pub fn char_offset(value: &str, byte_offset: usize) -> usize {
value[..byte_offset].chars().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_unique_match() -> Result<()> {
assert_eq!(TextSelection::new("world").resolve("hello world")?, 6..11);
let before = TextSelection::new("world").mode(SelectionMode::CursorBefore);
assert_eq!(before.resolve("hello world")?, 6..6);
let after = TextSelection::new("hello").mode(SelectionMode::CursorAfter);
assert_eq!(after.resolve("hello world")?, 5..5);
Ok(())
}
#[test]
fn disambiguates_with_prefix_and_suffix() -> Result<()> {
let value = "cat dog cat bird";
assert!(TextSelection::new("cat").resolve(value).is_err());
assert_eq!(TextSelection::new("cat").prefix("dog ").resolve(value)?, 8..11);
assert_eq!(TextSelection::new("cat").suffix(" dog").resolve(value)?, 0..3);
Ok(())
}
#[test]
fn reports_missing_text() {
assert!(TextSelection::new("zebra").resolve("cat").is_err());
assert!(TextSelection::new("").resolve("cat").is_err());
}
#[test]
fn converts_offsets() {
let value = "a😀b";
assert_eq!(utf16_offset(value, 5), 3);
assert_eq!(char_offset(value, 5), 2);
}
}