1use std::ops::Range;
4
5use crate::error::{Error, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9#[non_exhaustive]
10pub enum SelectionMode {
11 #[default]
13 Text,
14 CursorBefore,
16 CursorAfter,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct TextSelection {
23 pub text: String,
25 pub prefix: Option<String>,
27 pub suffix: Option<String>,
29 pub mode: SelectionMode,
31}
32
33impl TextSelection {
34 #[must_use]
36 pub fn new(text: impl Into<String>) -> Self {
37 Self { text: text.into(), ..Self::default() }
38 }
39
40 #[must_use]
42 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
43 self.prefix = Some(prefix.into());
44 self
45 }
46
47 #[must_use]
49 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
50 self.suffix = Some(suffix.into());
51 self
52 }
53
54 #[must_use]
56 pub const fn mode(mut self, mode: SelectionMode) -> Self {
57 self.mode = mode;
58 self
59 }
60
61 pub fn resolve(&self, value: &str) -> Result<Range<usize>> {
70 if self.text.is_empty() {
71 return Err(Error::invalid("text to select must not be empty"));
72 }
73 let prefix = self.prefix.as_deref().unwrap_or("");
74 let suffix = self.suffix.as_deref().unwrap_or("");
75 let mut matches = value.match_indices(&self.text).filter_map(|(start, m)| {
76 let end = start + m.len();
77 (value[..start].ends_with(prefix) && value[end..].starts_with(suffix)).then_some(start..end)
78 });
79 let found = matches
80 .next()
81 .ok_or_else(|| Error::invalid(format!("text {:?} was not found in the element's value", self.text)))?;
82 if matches.next().is_some() {
83 return Err(Error::invalid(format!(
84 "text {:?} occurs more than once; add a prefix or suffix to disambiguate",
85 self.text
86 )));
87 }
88 Ok(match self.mode {
89 SelectionMode::Text => found,
90 SelectionMode::CursorBefore => found.start..found.start,
91 SelectionMode::CursorAfter => found.end..found.end,
92 })
93 }
94}
95
96#[must_use]
98pub fn utf16_offset(value: &str, byte_offset: usize) -> usize {
99 value[..byte_offset].encode_utf16().count()
100}
101
102#[must_use]
104pub fn char_offset(value: &str, byte_offset: usize) -> usize {
105 value[..byte_offset].chars().count()
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn resolves_unique_match() -> Result<()> {
114 assert_eq!(TextSelection::new("world").resolve("hello world")?, 6..11);
115 let before = TextSelection::new("world").mode(SelectionMode::CursorBefore);
116 assert_eq!(before.resolve("hello world")?, 6..6);
117 let after = TextSelection::new("hello").mode(SelectionMode::CursorAfter);
118 assert_eq!(after.resolve("hello world")?, 5..5);
119 Ok(())
120 }
121
122 #[test]
123 fn disambiguates_with_prefix_and_suffix() -> Result<()> {
124 let value = "cat dog cat bird";
125 assert!(TextSelection::new("cat").resolve(value).is_err());
126 assert_eq!(TextSelection::new("cat").prefix("dog ").resolve(value)?, 8..11);
127 assert_eq!(TextSelection::new("cat").suffix(" dog").resolve(value)?, 0..3);
128 Ok(())
129 }
130
131 #[test]
132 fn reports_missing_text() {
133 assert!(TextSelection::new("zebra").resolve("cat").is_err());
134 assert!(TextSelection::new("").resolve("cat").is_err());
135 }
136
137 #[test]
138 fn converts_offsets() {
139 let value = "a😀b";
140 assert_eq!(utf16_offset(value, 5), 3);
141 assert_eq!(char_offset(value, 5), 2);
142 }
143}