1use super::analysis::{worker, AnalysisTarget};
6use super::Editor;
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use strop_core::id::{BufferRevision, DocumentId};
11use strop_core::worker::{Completion, FailureKind, Outcome, Ticket};
12use strop_core::Buffer;
13use strop_grammar::{delimiter_pair, matching_delimiter_at, MatchCancelled};
14
15fn near_delimiter(buf: &Buffer, caret: usize, insert: bool) -> bool {
20 let on = buf
21 .byte_at(caret)
22 .is_some_and(|byte| delimiter_pair(byte).is_some());
23 let typed = insert
24 && caret
25 .checked_sub(1)
26 .and_then(|before| buf.byte_at(before))
27 .is_some_and(|byte| delimiter_pair(byte).is_some());
28 on || typed
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub struct MatchKey {
36 pub target: AnalysisTarget,
37 pub revision: BufferRevision,
38 pub caret: usize,
39 pub insert: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44pub struct PairMatch {
45 pub first: usize,
46 pub second: usize,
47}
48
49struct PendingMatch {
50 ticket: Ticket<MatchKey>,
51 cancel: Arc<AtomicBool>,
52}
53
54#[derive(Default)]
57pub(crate) struct PairState {
58 pending: HashMap<AnalysisTarget, PendingMatch>,
59 cache: HashMap<AnalysisTarget, Vec<(MatchKey, Option<PairMatch>)>>,
60}
61
62impl PairState {
63 pub(crate) fn pending_empty(&self) -> bool {
64 self.pending.is_empty()
65 }
66
67 fn pending_covers(&self, key: &MatchKey) -> bool {
70 self.pending
71 .get(&key.target)
72 .is_some_and(|pending| pending.ticket.key == *key)
73 }
74
75 pub(crate) fn cancel_target(&mut self, target: &AnalysisTarget) {
78 if let Some(pending) = self.pending.remove(target) {
79 pending.cancel.store(true, Ordering::Release);
80 }
81 }
82
83 pub(crate) fn cancel_all(&mut self) {
84 for pending in self.pending.drain().map(|(_, pending)| pending) {
85 pending.cancel.store(true, Ordering::Release);
86 }
87 }
88
89 pub(crate) fn forget(&mut self, target: &AnalysisTarget) {
90 self.cancel_target(target);
91 self.cache.remove(target);
92 }
93
94 fn lookup(&self, key: &MatchKey) -> Option<Option<PairMatch>> {
95 self.cache
96 .get(&key.target)?
97 .iter()
98 .find(|(k, _)| k == key)
99 .map(|(_, value)| *value)
100 }
101
102 fn store(&mut self, key: MatchKey, value: Option<PairMatch>) {
103 let entries = self.cache.entry(key.target.clone()).or_default();
104 entries.retain(|(k, _)| *k != key);
105 const CACHED_MATCHES: usize = 8;
108 while entries.len() >= CACHED_MATCHES {
109 entries.remove(0);
110 }
111 entries.push((key, value));
112 }
113}
114
115pub(crate) fn match_delimiters(
117 buf: &Buffer,
118 caret: usize,
119 insert: bool,
120 cancelled: impl Fn() -> bool,
121) -> Result<Option<(usize, usize)>, MatchCancelled> {
122 if cancelled() {
123 return Err(MatchCancelled);
124 }
125 let probe = if buf.byte_at(caret).and_then(delimiter_pair).is_some() {
126 Some(caret)
127 } else {
128 caret
129 .checked_sub(1)
130 .filter(|_| insert)
131 .filter(|&at| buf.byte_at(at).and_then(delimiter_pair).is_some())
132 };
133 let Some(probe) = probe else { return Ok(None) };
134 matching_delimiter_at(buf, probe, cancelled)
135 .map(|mate| mate.map(|mate| (probe.min(mate), probe.max(mate))))
136}
137
138impl Editor {
139 pub fn pair_highlight(
146 &mut self,
147 doc: DocumentId,
148 caret: usize,
149 insert: bool,
150 ) -> [Option<usize>; 2] {
151 const NONE: [Option<usize>; 2] = [None, None];
152 if self.finishing {
153 return NONE;
154 }
155 let Some((source, source_caret)) = self.source_position(doc, caret) else {
158 return NONE;
159 };
160 let near = match self.docs.get(source) {
161 Some(document) => near_delimiter(&document.buf, source_caret, insert),
162 None => return NONE,
163 };
164 if !near {
165 self.analysis
167 .pair
168 .cancel_target(&AnalysisTarget::Document(source));
169 return NONE;
170 }
171 let Some(document) = self.docs.get(source) else {
172 return NONE;
173 };
174 let key = MatchKey {
175 target: AnalysisTarget::Document(source),
176 revision: document.buf.revision(),
177 caret: source_caret,
178 insert,
179 };
180 let rope = document.buf.snapshot();
181 match self.analysis.pair.lookup(&key) {
182 Some(Some(pair)) => [
183 self.view_byte_for_source(doc, source, pair.first),
184 self.view_byte_for_source(doc, source, pair.second),
185 ],
186 Some(None) => NONE,
187 None => {
188 self.request_match(key, rope);
189 NONE
190 }
191 }
192 }
193
194 fn view_byte_for_source(
200 &self,
201 doc: DocumentId,
202 source: DocumentId,
203 byte: usize,
204 ) -> Option<usize> {
205 if !self.collections.contains_key(&doc) {
206 return (source == doc).then_some(byte);
207 }
208 let collection = self.collections.get(&doc)?;
209 let source_buf = &self.docs.get(source)?.buf;
210 let view_buf = &self.docs.get(doc)?.buf;
211 let target_line = source_buf.line_of(byte.min(source_buf.len_bytes()));
212 for excerpt in &collection.excerpts {
213 if excerpt.source != source {
214 continue;
215 }
216 let first_line = source_buf.line_of(excerpt.start);
217 if target_line < first_line || target_line >= first_line + excerpt.view_lines {
218 continue;
219 }
220 let view_line = excerpt.view_line + 1 + (target_line - first_line);
221 if view_line > view_buf.last_content_line() {
222 return None;
223 }
224 let col = byte.saturating_sub(source_buf.line_start(target_line));
225 let start = view_buf.line_start(view_line);
226 return Some((start + col).min(view_buf.line_end(view_line)));
227 }
228 None
229 }
230
231 fn request_match(&mut self, key: MatchKey, rope: ropey::Rope) {
234 if self.analysis.pair.pending_covers(&key) {
235 return; }
237 if let Err(error) = self.analysis.start(&self.tape) {
238 self.message = format!("analysis: {error}");
239 return;
240 }
241 self.analysis.pair.cancel_target(&key.target);
242 let request = match self.worker_ids.allocate() {
243 Ok(id) => id,
244 Err(error) => {
245 self.message = error.message;
246 return;
247 }
248 };
249 let ticket = Ticket {
250 request,
251 key: key.clone(),
252 };
253 let cancel = Arc::new(AtomicBool::new(false));
254 self.analysis.register(key.target.clone());
255 self.analysis.pair.pending.insert(
256 key.target.clone(),
257 PendingMatch {
258 ticket: ticket.clone(),
259 cancel: cancel.clone(),
260 },
261 );
262 match self.tape.request("analysis.match", &ticket) {
263 Ok(false) => return,
264 Ok(true) => {}
265 Err(error) => {
266 self.handle_match(Completion {
267 ticket,
268 outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
269 });
270 return;
271 }
272 }
273 let work = worker::MatchWork {
274 ticket,
275 rope,
276 cancel,
277 };
278 let failed = match self.analysis.worker() {
279 Some(worker) => worker.match_work(work).err(),
280 None => Some(Box::new(work)),
281 };
282 if let Some(work) = failed {
283 self.handle_match(Completion {
284 ticket: work.ticket,
285 outcome: Outcome::failed(
286 FailureKind::Disconnected,
287 "display analysis worker stopped",
288 ),
289 });
290 }
291 }
292
293 pub(crate) fn handle_match(&mut self, completion: Completion<MatchKey, Option<PairMatch>>) {
300 let key = completion.ticket.key;
301 if !self
302 .analysis
303 .pair
304 .pending
305 .get(&key.target)
306 .is_some_and(|pending| pending.ticket.request == completion.ticket.request)
307 {
308 return;
309 }
310 self.analysis.pair.pending.remove(&key.target);
311 let current = match &key.target {
312 AnalysisTarget::Document(document) => self
313 .docs
314 .get(*document)
315 .is_some_and(|document| document.buf.revision() == key.revision),
316 AnalysisTarget::Preview(path) => self.previews.contains_key(path),
317 };
318 if !current {
319 return;
320 }
321 match completion.outcome {
322 Outcome::Success(value) => self.analysis.pair.store(key, value),
323 Outcome::Failed { .. } => self.analysis.pair.store(key, None),
324 Outcome::Cancelled(_) => {}
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests;