1pub(crate) mod layouts;
4mod search;
5mod worker;
6use super::{document::DocumentSource, Editor};
7use std::collections::{HashMap, HashSet};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{mpsc, Arc};
11use strop_core::id::{BufferRevision, DocumentId};
12use strop_core::worker::{Completion, Outcome, Ticket};
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub enum AnalysisTarget {
16 Document(DocumentId),
17 Preview(#[serde(with = "strop_core::path_serde")] PathBuf),
18}
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct AnalysisKey {
21 pub target: AnalysisTarget,
22 pub revision: BufferRevision,
23 pub first: usize,
24 pub last: usize,
25 pub tab: usize,
26 pub guides: bool,
27 pub left: usize,
28 pub right: usize,
29 #[serde(with = "strop_core::path_serde::option")]
30 pub syntax_path: Option<PathBuf>,
31 pub search: Option<strop_grammar::CompiledQuery>,
32}
33#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
34pub struct FrameAnalysis {
35 pub spans: Vec<strop_syntax::Span>,
36 pub guides: strop_syntax::GuideFrame,
37 pub search: Option<Result<SearchSummary, String>>,
38 pub layouts: Vec<strop_core::layout::PreparedLineLayout>,
39}
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
41pub struct SearchSummary {
42 pub count: usize,
43 pub hits: Vec<strop_grammar::SearchMatch>,
44}
45#[derive(serde::Serialize, serde::Deserialize)]
46pub enum AnalysisEvent {
47 Completed(Box<Completion<AnalysisKey, FrameAnalysis>>),
48 Stopped,
49}
50struct Pending {
51 ticket: Ticket<AnalysisKey>,
52 cancel: Arc<AtomicBool>,
53}
54struct Cached {
55 key: AnalysisKey,
56 value: Option<Arc<FrameAnalysis>>,
57}
58pub(crate) struct AnalysisState {
59 pub tx: mpsc::Sender<AnalysisEvent>,
60 pub rx: Option<mpsc::Receiver<AnalysisEvent>>,
61 worker: Option<worker::Worker>,
62 registered: HashSet<AnalysisTarget>,
63 pending: HashMap<AnalysisTarget, Pending>,
64 cache: HashMap<AnalysisTarget, Vec<Cached>>,
65 started: bool,
66 stopping: bool,
67}
68impl Default for AnalysisState {
69 fn default() -> Self {
70 let (tx, rx) = mpsc::channel();
71 Self {
72 tx,
73 rx: Some(rx),
74 worker: None,
75 registered: HashSet::new(),
76 pending: HashMap::new(),
77 cache: HashMap::new(),
78 started: false,
79 stopping: false,
80 }
81 }
82}
83impl AnalysisState {
84 pub fn pending(&self) -> bool {
85 !self.pending.is_empty() || self.stopping
86 }
87 fn start(&mut self, tape: &strop_trace::replay::Tape) -> Result<(), String> {
88 if self.started {
89 return Ok(());
90 }
91 let result: Result<(), String> = tape
92 .call("analysis.start", &(), || {
93 worker::Worker::start(self.tx.clone())
94 .map(|worker| self.worker = Some(worker))
95 .map_err(|error| error.to_string())
96 })
97 .map_err(|error| error.to_string())?;
98 result?;
99 self.started = true;
100 Ok(())
101 }
102 pub fn edits(&mut self, document: DocumentId, changes: &[strop_core::Change]) {
103 let target = AnalysisTarget::Document(document);
104 if !self.registered.contains(&target) {
105 return;
106 }
107 if let Some(pending) = self.pending.get(&target) {
108 pending.cancel.store(true, Ordering::Release);
109 }
110 if let Some(worker) = &self.worker {
111 if !worker.edits(target, changes.to_vec()) {
112 self.started = false;
113 }
114 }
115 }
116
117 pub fn forget(&mut self, target: AnalysisTarget) {
118 if let Some(pending) = self.pending.remove(&target) {
119 pending.cancel.store(true, Ordering::Release);
120 }
121 self.cache.remove(&target);
122 self.registered.remove(&target);
123 if let Some(worker) = &self.worker {
124 if !worker.forget(target) {
125 self.started = false;
126 }
127 }
128 }
129
130 pub fn stop(&mut self) {
131 for pending in self.pending.values() {
132 pending.cancel.store(true, Ordering::Release);
133 }
134 self.stopping = self.started;
135 self.worker = None;
136 }
137}
138
139pub fn clip_stale_frame(frame: Arc<FrameAnalysis>, len: usize) -> Arc<FrameAnalysis> {
144 let mut clipped = (*frame).clone();
145 clipped.spans.retain(|span| span.start < len);
146 for span in &mut clipped.spans {
147 span.end = span.end.min(len);
148 }
149 Arc::new(clipped)
150}
151impl Editor {
152 pub fn document_analysis(
153 &mut self,
154 document: DocumentId,
155 first: usize,
156 last: usize,
157 left: usize,
158 width: usize,
159 ) -> Option<Arc<FrameAnalysis>> {
160 if self.finishing {
161 return None;
162 }
163 let doc = self.docs.get(document)?;
164 let guides = self.config.indent_guides
165 && matches!(
166 doc.source,
167 DocumentSource::File | DocumentSource::Scratch | DocumentSource::Remote(_)
168 );
169 let target = AnalysisTarget::Document(document);
170 let search = if document == self.current() {
171 self.current_search_query().ok().flatten()
172 } else {
173 None
174 };
175 if !guides && doc.syntax_path().is_none() && search.is_none() {
176 return None;
177 }
178 let key = AnalysisKey {
179 target,
180 revision: doc.buf.revision(),
181 first,
182 last,
183 tab: self.cur_indent().width.max(1),
184 guides,
185 left,
186 right: left.saturating_add(width),
187 syntax_path: doc.syntax_path().map(std::path::Path::to_path_buf),
188 search,
189 };
190 if let Some(cached) = self
191 .analysis
192 .cache
193 .get(&key.target)
194 .and_then(|entries| entries.iter().find(|entry| entry.key == key))
195 {
196 return cached.value.clone();
197 }
198 let stale = self
203 .analysis
204 .cache
205 .get(&key.target)
206 .and_then(|entries| {
207 entries
208 .iter()
209 .rev()
210 .find(|entry| {
211 entry.key.first == key.first
212 && entry.key.last == key.last
213 && entry.key.tab == key.tab
214 && entry.key.search == key.search
215 && entry.key.revision.get() < key.revision.get()
216 && entry.value.is_some()
217 })
218 .and_then(|entry| entry.value.clone())
219 })
220 .map(|frame| clip_stale_frame(frame, doc.buf.len_bytes()));
221 if let Some(pending) = self.analysis.pending.get(&key.target) {
222 if pending.ticket.key.revision != key.revision
223 || pending.ticket.key.search != key.search
224 {
225 pending.cancel.store(true, Ordering::Release);
226 }
227 return stale;
228 }
229 let rope = doc.buf.snapshot();
230 self.request_analysis(key, rope);
231 stale
232 }
233
234 pub fn preview_analysis(
235 &mut self,
236 path: &std::path::Path,
237 first: usize,
238 last: usize,
239 width: usize,
240 ) -> Option<Arc<FrameAnalysis>> {
241 if self.finishing {
242 return None;
243 }
244 let entry = self.previews.get(path)?;
245 let key = AnalysisKey {
246 target: AnalysisTarget::Preview(path.to_path_buf()),
247 revision: BufferRevision::new(0),
248 first,
249 last,
250 tab: self.cur_indent().width.max(1),
251 guides: false,
252 left: 0,
253 right: width,
254 syntax_path: Some(path.to_path_buf()),
255 search: None,
256 };
257 if let Some(cached) = self
258 .analysis
259 .cache
260 .get(&key.target)
261 .and_then(|entries| entries.iter().find(|entry| entry.key == key))
262 {
263 return cached.value.clone();
264 }
265 if self.analysis.pending.contains_key(&key.target) {
266 return None;
267 }
268 let rope = entry.rope.clone();
269 self.request_analysis(key, rope);
270 None
271 }
272
273 pub fn search_summary(
274 &self,
275 query: &strop_grammar::CompiledQuery,
276 ) -> Option<&Result<SearchSummary, String>> {
277 self.analysis
278 .cache
279 .get(&AnalysisTarget::Document(self.current()))?
280 .iter()
281 .rev()
282 .find(|entry| {
283 entry.key.revision == self.buf().revision()
284 && entry.key.search.as_ref() == Some(query)
285 })?
286 .value
287 .as_ref()?
288 .search
289 .as_ref()
290 }
291
292 fn request_analysis(&mut self, key: AnalysisKey, rope: ropey::Rope) {
293 if let Err(error) = self.analysis.start(&self.tape) {
294 self.message = format!("analysis: {error}");
295 self.analysis
296 .cache
297 .entry(key.target.clone())
298 .or_default()
299 .push(Cached { key, value: None });
300 return;
301 }
302 let request = match self.worker_ids.allocate() {
303 Ok(id) => id,
304 Err(error) => {
305 self.message = error.message;
306 return;
307 }
308 };
309 let ticket = Ticket {
310 request,
311 key: key.clone(),
312 };
313 let cancel = Arc::new(AtomicBool::new(false));
314 self.analysis.registered.insert(key.target.clone());
315 self.analysis.pending.insert(
316 key.target.clone(),
317 Pending {
318 ticket: ticket.clone(),
319 cancel: cancel.clone(),
320 },
321 );
322 match self.tape.request("analysis.viewport", &ticket) {
323 Ok(false) => return,
324 Ok(true) => {}
325 Err(error) => {
326 self.handle_analysis(AnalysisEvent::Completed(Box::new(Completion {
327 ticket,
328 outcome: Outcome::failed(
329 strop_core::worker::FailureKind::Protocol,
330 error.to_string(),
331 ),
332 })));
333 return;
334 }
335 }
336 let work = worker::Work {
337 ticket,
338 rope,
339 cancel,
340 };
341 let failed = match &self.analysis.worker {
342 Some(worker) => worker.analyze(work).err(),
343 None => Some(Box::new(work)),
344 };
345 if let Some(work) = failed {
346 self.handle_analysis(AnalysisEvent::Completed(Box::new(Completion {
347 ticket: work.ticket,
348 outcome: Outcome::failed(
349 strop_core::worker::FailureKind::Disconnected,
350 "display analysis worker stopped",
351 ),
352 })));
353 }
354 }
355
356 pub(crate) fn handle_analysis(&mut self, event: AnalysisEvent) {
357 let AnalysisEvent::Completed(completion) = event else {
358 self.analysis.stopping = false;
359 self.analysis.started = false;
360 return;
361 };
362 let key = completion.ticket.key;
363 if !self
364 .analysis
365 .pending
366 .get(&key.target)
367 .is_some_and(|pending| pending.ticket.request == completion.ticket.request)
368 {
369 return;
370 }
371 self.analysis.pending.remove(&key.target);
372 let current = match &key.target {
373 AnalysisTarget::Document(document) => self
374 .docs
375 .get(*document)
376 .is_some_and(|document| document.buf.revision() == key.revision),
377 AnalysisTarget::Preview(path) => self.previews.contains_key(path),
378 };
379 if !current {
380 return;
381 }
382 let value = match completion.outcome {
383 Outcome::Success(frame) => {
384 if let AnalysisTarget::Document(document) = key.target {
385 if let Some(document) = self.docs.get_mut(document) {
386 if !document
387 .buf
388 .install_line_layouts(key.revision, &frame.layouts)
389 {
390 self.message = "invalid layout publication".into();
391 return;
392 }
393 }
394 }
395 Some(Arc::new(frame))
396 }
397 Outcome::Failed { failure, .. } => {
398 self.message = format!("analysis: {}", failure.message);
399 None
400 }
401 Outcome::Cancelled(_) => return,
402 };
403 let entries = self.analysis.cache.entry(key.target.clone()).or_default();
404 let previous = key.revision.get().saturating_sub(1);
408 entries.retain(|entry| entry.key.revision.get() >= previous && entry.key.tab == key.tab);
409 const CACHED_WINDOWS: usize = 8;
410 if entries.len() == CACHED_WINDOWS {
411 entries.remove(0);
412 }
413 entries.push(Cached { key, value });
414 }
415}
416
417#[cfg(any(test, feature = "test-support"))]
418impl Editor {
419 pub fn analysis_fixture(&mut self) -> Arc<FrameAnalysis> {
420 loop {
421 if let Some(frame) =
422 self.document_analysis(self.current(), 0, self.buf().len_bytes(), 0, 80)
423 {
424 return frame;
425 }
426 let event = self
427 .analysis
428 .rx
429 .as_ref()
430 .expect("unforwarded analysis")
431 .recv_timeout(std::time::Duration::from_secs(5))
432 .expect("analysis completion");
433 self.handle_analysis(event);
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use strop_core::worker::WorkerId;
442 use strop_core::Buffer;
443 use strop_syntax::{Class, Emphasis, Span};
444
445 #[test]
449 fn an_edit_serves_the_previous_frame_instead_of_blanking() {
450 let mut e = Editor::new(Buffer::from_text("fn main() {}\n"));
451 e.buf_mut().path = Some(PathBuf::from("/workspace/a.rs"));
452 let doc = e.current();
453 let target = AnalysisTarget::Document(doc);
454 let revision = e.buf().revision();
455 let key = AnalysisKey {
456 target: target.clone(),
457 revision,
458 first: 0,
459 last: 0,
460 tab: 4,
461 guides: true,
462 left: 0,
463 right: 100,
464 syntax_path: Some(PathBuf::from("/workspace/a.rs")),
465 search: None,
466 };
467 let frame = FrameAnalysis {
468 spans: vec![Span {
469 start: 0,
470 end: 2,
471 class: Class::Keyword,
472 emphasis: Emphasis::default(),
473 }],
474 ..Default::default()
475 };
476 e.analysis.pending.insert(
477 target.clone(),
478 Pending {
479 ticket: Ticket {
480 request: WorkerId::new(1),
481 key: key.clone(),
482 },
483 cancel: Arc::new(AtomicBool::new(false)),
484 },
485 );
486 e.handle_analysis(AnalysisEvent::Completed(Box::new(Completion {
487 ticket: Ticket {
488 request: WorkerId::new(1),
489 key,
490 },
491 outcome: Outcome::Success(frame),
492 })));
493 assert!(e.document_analysis(doc, 0, 0, 0, 100).is_some());
494 e.feed_text("x"); let served = e.document_analysis(doc, 0, 0, 0, 100);
496 assert!(
497 served.is_some(),
498 "an interim frame serves the previous analysis"
499 );
500 e.feed_text("0wD");
502 let _ = e.document_analysis(doc, 0, 0, 0, 100);
503 }
504}