1use crate::{Cursor, Doc, Selection};
21
22const CONTEXT: usize = 32;
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub struct Quote {
28 pub exact: String,
29 pub prefix: String,
31 pub suffix: String,
33}
34
35impl Quote {
36 pub fn of(doc: &Doc, range: Selection) -> Option<Self> {
41 let flat = Flat::new(doc);
42 let (start, end) = range.clamp(doc).ordered();
43 let (start, end) = (flat.index(start)?, flat.index(end)?);
44 if start >= end {
45 return None;
46 }
47 let text = flat.text.as_str();
48 Some(Self {
49 exact: text[start..end].to_string(),
50 prefix: tail(&text[..start], CONTEXT).to_string(),
51 suffix: head(&text[end..], CONTEXT).to_string(),
52 })
53 }
54
55 pub fn find(&self, doc: &Doc, hint: Option<Selection>) -> Option<Selection> {
62 if self.exact.is_empty() {
63 return None;
64 }
65 let flat = Flat::new(doc);
66 let text = flat.text.as_str();
67 let near = hint.and_then(|hint| flat.index(hint.clamp(doc).ordered().0));
68 if let Some(at) = near
69 && text
70 .get(at..)
71 .is_some_and(|rest| rest.starts_with(&self.exact))
72 {
73 return flat.selection(at, at + self.exact.len());
74 }
75
76 let at = occurrences(text, &self.exact).max_by_key(|&at| {
77 let end = at + self.exact.len();
78 let context =
79 common_tail(&text[..at], &self.prefix) + common_head(&text[end..], &self.suffix);
80 let distance = near.map_or(0, |near| near.abs_diff(at));
81 (context, std::cmp::Reverse(distance))
82 })?;
83 flat.selection(at, at + self.exact.len())
84 }
85}
86
87struct Flat {
89 text: String,
90 parts: Vec<(usize, Cursor)>,
92}
93
94impl Flat {
95 fn new(doc: &Doc) -> Self {
96 let mut text = String::new();
97 let mut parts = Vec::new();
98 for (ix, block) in doc.blocks.iter().enumerate() {
99 for part in block.parts() {
100 let Some(body) = block.text_at(part) else {
101 continue;
102 };
103 if !parts.is_empty() {
104 text.push('\n');
105 }
106 parts.push((text.len(), Cursor::new(ix, part, 0)));
107 text.push_str(&body.text);
108 }
109 }
110 Self { text, parts }
111 }
112
113 fn index(&self, cursor: Cursor) -> Option<usize> {
115 let at = self
116 .parts
117 .iter()
118 .position(|(_, start)| start.block == cursor.block && start.part == cursor.part)?;
119 let (start, _) = self.parts[at];
120 let end = self
121 .parts
122 .get(at + 1)
123 .map_or(self.text.len(), |(next, _)| next - 1);
124 Some((start + cursor.offset).min(end))
125 }
126
127 fn cursor(&self, at: usize) -> Option<Cursor> {
129 let (start, cursor) = self.parts.iter().rev().find(|(start, _)| *start <= at)?;
130 Some(Cursor {
131 offset: at - start,
132 ..*cursor
133 })
134 }
135
136 fn selection(&self, start: usize, end: usize) -> Option<Selection> {
137 Some(Selection::new(self.cursor(start)?, self.cursor(end)?))
138 }
139}
140
141fn occurrences<'a>(text: &'a str, exact: &'a str) -> impl Iterator<Item = usize> + 'a {
143 text.char_indices()
144 .map(|(at, _)| at)
145 .filter(move |&at| text[at..].starts_with(exact))
146}
147
148fn tail(text: &str, count: usize) -> &str {
150 let at = text
151 .char_indices()
152 .rev()
153 .nth(count.saturating_sub(1))
154 .map_or(0, |(at, _)| at);
155 &text[at..]
156}
157
158fn head(text: &str, count: usize) -> &str {
160 let at = text
161 .char_indices()
162 .nth(count)
163 .map_or(text.len(), |(at, _)| at);
164 &text[..at]
165}
166
167fn common_tail(a: &str, b: &str) -> usize {
169 a.chars()
170 .rev()
171 .zip(b.chars().rev())
172 .take_while(|(a, b)| a == b)
173 .count()
174}
175
176fn common_head(a: &str, b: &str) -> usize {
178 a.chars().zip(b.chars()).take_while(|(a, b)| a == b).count()
179}