1use ahash::AHashMap;
11use bonsai_common::FileId;
12use bonsai_diagnostics::{Diagnostic, Severity};
13use bonsai_lang_api::{AdapterArc, AdapterError, LanguageId};
14use bonsai_vfs::{FileSnapshot, Vfs};
15use parking_lot::{Mutex, RwLock};
16use std::{
17 ops::{ControlFlow, Deref, DerefMut},
18 sync::Arc,
19 time::Duration,
20};
21use thiserror::Error;
22use tree_sitter::{InputEdit, Node, ParseOptions, Parser, Point, Tree};
23
24type ParseKey = (u64, FileId, LanguageId, &'static str);
25type ParserPool = Arc<Mutex<Vec<Parser>>>;
26
27struct ParserLease {
31 parser: Option<Parser>,
32 pool: ParserPool,
33}
34
35impl Deref for ParserLease {
36 type Target = Parser;
37
38 fn deref(&self) -> &Self::Target {
39 self.parser.as_ref().expect("parser lease is populated")
40 }
41}
42
43impl DerefMut for ParserLease {
44 fn deref_mut(&mut self) -> &mut Self::Target {
45 self.parser.as_mut().expect("parser lease is populated")
46 }
47}
48
49impl Drop for ParserLease {
50 fn drop(&mut self) {
51 if let Some(parser) = self.parser.take() {
52 self.pool.lock().push(parser);
53 }
54 }
55}
56
57#[derive(Debug, Error)]
58pub enum ParseError {
59 #[error(transparent)]
60 Adapter(#[from] AdapterError),
61 #[error("no language adapter handles file {0:?}")]
62 NoAdapter(FileId),
63 #[error("vfs: {0}")]
64 Vfs(#[from] bonsai_vfs::VfsError),
65}
66
67#[derive(Clone)]
68pub struct ParsedFile {
69 pub file: FileId,
70 pub version: u64,
71 pub tree: Arc<Tree>,
72 pub diagnostics: Vec<Diagnostic>,
73 pub adapter_id: bonsai_lang_api::LanguageId,
74 pub grammar_name: &'static str,
76 source: Arc<str>,
77 used_recovery: bool,
78}
79
80impl std::fmt::Debug for ParsedFile {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("ParsedFile")
83 .field("file", &self.file)
84 .field("version", &self.version)
85 .field("adapter_id", &self.adapter_id)
86 .field("grammar_name", &self.grammar_name)
87 .field("diagnostics", &self.diagnostics.len())
88 .field("used_recovery", &self.used_recovery)
89 .finish()
90 }
91}
92
93impl ParsedFile {
94 #[must_use]
101 pub fn source_text(&self) -> &str {
102 &self.source
103 }
104}
105
106#[derive(Copy, Clone, Debug, Eq, PartialEq)]
112pub struct ParserOptions {
113 pub parse_timeout: Option<Duration>,
114}
115
116impl Default for ParserOptions {
117 fn default() -> Self {
118 Self {
119 parse_timeout: parse_timeout_from_env(),
120 }
121 }
122}
123
124impl ParserOptions {
125 #[must_use]
126 pub fn with_parse_timeout(timeout: Option<Duration>) -> Self {
127 Self {
128 parse_timeout: timeout,
129 }
130 }
131}
132
133#[derive(Clone)]
142pub struct ParserCache {
143 parsers: Arc<Mutex<AHashMap<&'static str, ParserPool>>>,
144 cache: Arc<RwLock<AHashMap<ParseKey, Arc<ParsedFile>>>>,
145 options: ParserOptions,
146}
147
148impl Default for ParserCache {
149 fn default() -> Self {
150 Self::with_options(ParserOptions::default())
151 }
152}
153
154impl std::fmt::Debug for ParserCache {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 let cached_files = self.cache.read().len();
162 let pools = self.parsers.lock().values().cloned().collect::<Vec<_>>();
163 let idle_parsers = pools.iter().map(|pool| pool.lock().len()).sum::<usize>();
164 f.debug_struct("ParserCache")
165 .field("cached_files", &cached_files)
166 .field("parser_grammars", &pools.len())
167 .field("idle_parsers", &idle_parsers)
168 .field("parse_timeout", &self.options.parse_timeout)
169 .finish()
170 }
171}
172
173impl ParserCache {
174 #[must_use]
177 pub fn new() -> Self {
178 Self::default()
179 }
180
181 #[must_use]
184 pub fn with_options(options: ParserOptions) -> Self {
185 Self {
186 parsers: Arc::new(Mutex::new(AHashMap::new())),
187 cache: Arc::new(RwLock::new(AHashMap::new())),
188 options,
189 }
190 }
191
192 pub fn parse(
195 &self,
196 file: FileId,
197 adapter: &AdapterArc,
198 vfs: &Vfs,
199 ) -> Result<Arc<ParsedFile>, ParseError> {
200 let snapshot = vfs.snapshot(file)?;
201 self.parse_snapshot(&snapshot, adapter, vfs)
202 }
203
204 pub fn parse_snapshot(
210 &self,
211 snapshot: &FileSnapshot,
212 adapter: &AdapterArc,
213 vfs: &Vfs,
214 ) -> Result<Arc<ParsedFile>, ParseError> {
215 let file = snapshot.file_id;
216 let path = vfs.path(file)?;
217 let grammar_name = adapter.grammar_name_for_path(&path);
218 let key = (vfs.instance_id(), file, adapter.language_id(), grammar_name);
219 if let Some(entry) = self.cache.read().get(&key).cloned() {
220 if parsed_matches_snapshot(&entry, snapshot) {
221 return Ok(entry);
222 }
223 }
224
225 let language = adapter.tree_sitter_language_for_path(&path)?;
226 let mut parser = self.checkout_parser(grammar_name);
227 if let Some(entry) = self.cache.read().get(&key).cloned() {
230 if parsed_matches_snapshot(&entry, snapshot) {
231 return Ok(entry);
232 }
233 }
234 let old = self.cache.read().get(&key).cloned();
235 parser
236 .set_language(&language)
237 .map_err(|e| AdapterError::ParserSetup(e.to_string()))?;
238
239 let incremental_tree = old
240 .as_deref()
241 .and_then(|parsed| incremental_tree(parsed, &snapshot.text));
242 let old_tree = incremental_tree.as_ref();
243 let (mut tree, timed_out) = parse_with_timeout(
244 &mut parser,
245 snapshot.text.as_ref(),
246 old_tree,
247 self.options.parse_timeout,
248 )?;
249 let mut used_recovery = false;
250 if timed_out.is_none() && tree.root_node().has_error() {
251 let mut recovery_source = snapshot.text.as_bytes().to_vec();
252 loop {
253 let edits = adapter.parse_recovery_edits(snapshot, vfs, &tree);
254 if !apply_recovery_edits(snapshot.text.as_ref(), &mut recovery_source, &edits) {
255 break;
256 }
257 let recovery_text = std::str::from_utf8(&recovery_source)
258 .expect("same-width recovery normalization preserves UTF-8");
259 let (candidate, candidate_timed_out) =
260 parse_with_timeout(&mut parser, recovery_text, None, self.options.parse_timeout)?;
261 if candidate_timed_out.is_some()
262 || bonsai_lang_api::syntax_damage_score(&candidate)
263 >= bonsai_lang_api::syntax_damage_score(&tree)
264 {
265 break;
266 }
267 tree = candidate;
268 used_recovery = true;
269 }
270 }
271 drop(parser);
272 drop(incremental_tree);
273
274 let diagnostics = diagnostics_for_tree(file, snapshot.text.len(), &tree, timed_out);
275
276 let parsed = Arc::new(ParsedFile {
277 file,
278 version: snapshot.version,
279 tree: Arc::new(tree),
280 diagnostics,
281 adapter_id: adapter.language_id(),
282 grammar_name,
283 source: Arc::clone(&snapshot.text),
284 used_recovery,
285 });
286 let mut cache = self.cache.write();
290 if let Some(existing) = cache.get(&key) {
291 if parsed_matches_snapshot(existing, snapshot) {
292 return Ok(existing.clone());
293 }
294 if existing.version >= parsed.version {
295 return Ok(parsed);
296 }
297 }
298 cache.insert(key, parsed.clone());
299 Ok(parsed)
300 }
301
302 pub fn release(&self, file: FileId, adapter: &AdapterArc, vfs: &Vfs) {
310 let Ok(path) = vfs.path(file) else {
311 return;
312 };
313 self.cache.write().remove(&(
314 vfs.instance_id(),
315 file,
316 adapter.language_id(),
317 adapter.grammar_name_for_path(&path),
318 ));
319 }
320
321 pub fn invalidate(&self, file: FileId) {
323 self.cache
324 .write()
325 .retain(|(_, cached_file, _, _), _| *cached_file != file);
326 }
327
328 fn checkout_parser(&self, grammar_name: &'static str) -> ParserLease {
329 let pool = self
330 .parsers
331 .lock()
332 .entry(grammar_name)
333 .or_insert_with(|| Arc::new(Mutex::new(Vec::new())))
334 .clone();
335 let parser = pool.lock().pop().unwrap_or_default();
336 ParserLease {
337 parser: Some(parser),
338 pool,
339 }
340 }
341}
342
343fn parsed_matches_snapshot(parsed: &ParsedFile, snapshot: &FileSnapshot) -> bool {
344 parsed.version == snapshot.version && Arc::ptr_eq(&parsed.source, &snapshot.text)
345}
346
347fn incremental_tree(parsed: &ParsedFile, new_source: &str) -> Option<Tree> {
352 if parsed.used_recovery
353 || parsed
354 .diagnostics
355 .iter()
356 .any(|diagnostic| diagnostic.code.as_deref() == Some("parse-timeout"))
357 {
358 return None;
361 }
362 let mut tree = parsed.tree.as_ref().clone();
363 if parsed.source.as_ref() != new_source {
364 tree.edit(&single_replacement_edit(&parsed.source, new_source));
365 }
366 Some(tree)
367}
368
369fn apply_recovery_edits(
370 source: &str,
371 recovered: &mut [u8],
372 edits: &[bonsai_lang_api::ParseRecoveryEdit],
373) -> bool {
374 let mut changed = false;
375 for edit in edits {
376 changed |= edit.apply_to(source, recovered);
377 }
378 changed
379}
380
381fn diagnostics_for_tree(
382 file: FileId,
383 text_len: usize,
384 tree: &Tree,
385 timed_out: Option<Duration>,
386) -> Vec<Diagnostic> {
387 let mut diagnostics = Vec::new();
388 if let Some(timeout) = timed_out {
389 diagnostics.push(parse_timeout_diagnostic(file, text_len, timeout));
390 return diagnostics;
391 }
392 if !tree.root_node().has_error() {
393 return diagnostics;
394 }
395
396 let mut stack = vec![tree.root_node()];
402 while let Some(node) = stack.pop() {
403 let is_error = node.is_error();
404 let is_missing = node.is_missing();
405 if is_error || is_missing {
406 let span = span_for_node(file, node);
407 let msg = if is_missing {
408 format!("missing `{}`", node.kind())
409 } else {
410 "syntax error".to_string()
411 };
412 diagnostics.push(Diagnostic::new(span, Severity::Warning, msg).with_code("syntax-error"));
413 }
414 if !is_error {
415 let mut cursor = node.walk();
416 for child in node.children(&mut cursor) {
417 if child.has_error() || child.is_missing() {
418 stack.push(child);
419 }
420 }
421 }
422 }
423 if diagnostics.is_empty() {
424 diagnostics.push(
425 Diagnostic::new(
426 file_span(file, text_len),
427 Severity::Warning,
428 "syntax errors present",
429 )
430 .with_code("syntax-error"),
431 );
432 }
433 diagnostics
434}
435
436fn single_replacement_edit(old: &str, new: &str) -> InputEdit {
441 let old_bytes = old.as_bytes();
442 let new_bytes = new.as_bytes();
443 let mut prefix = old_bytes
444 .iter()
445 .zip(new_bytes)
446 .take_while(|(old, new)| old == new)
447 .count();
448 while prefix > 0 && (!old.is_char_boundary(prefix) || !new.is_char_boundary(prefix)) {
449 prefix -= 1;
450 }
451
452 let max_suffix = old
453 .len()
454 .saturating_sub(prefix)
455 .min(new.len().saturating_sub(prefix));
456 let mut suffix = old_bytes[old.len() - max_suffix..]
457 .iter()
458 .rev()
459 .zip(new_bytes[new.len() - max_suffix..].iter().rev())
460 .take_while(|(old, new)| old == new)
461 .count();
462 while suffix > 0
463 && (!old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix))
464 {
465 suffix -= 1;
466 }
467
468 let old_end = old.len() - suffix;
469 let new_end = new.len() - suffix;
470 InputEdit {
471 start_byte: prefix,
472 old_end_byte: old_end,
473 new_end_byte: new_end,
474 start_position: point_at_byte(old, prefix),
475 old_end_position: point_at_byte(old, old_end),
476 new_end_position: point_at_byte(new, new_end),
477 }
478}
479
480fn point_at_byte(text: &str, byte: usize) -> Point {
481 debug_assert!(byte <= text.len());
482 debug_assert!(text.is_char_boundary(byte));
483 let prefix = &text.as_bytes()[..byte];
484 let mut row = 0usize;
485 let mut line_start = 0usize;
486 for (index, value) in prefix.iter().enumerate() {
487 if *value == b'\n' {
488 row += 1;
489 line_start = index + 1;
490 }
491 }
492 let column = byte - line_start;
493 Point::new(row, column)
494}
495
496fn parse_timeout_from_env() -> Option<Duration> {
500 let Ok(raw) = std::env::var("BONSAI_PARSE_TIMEOUT_MS") else {
501 return None;
502 };
503 parse_timeout_millis(raw.trim().parse().ok()?)
504}
505
506fn parse_timeout_millis(ms: u64) -> Option<Duration> {
509 if ms == 0 {
510 None
511 } else {
512 Some(Duration::from_millis(ms))
513 }
514}
515
516fn parse_with_timeout(
520 parser: &mut Parser,
521 text: &str,
522 old_tree: Option<&Tree>,
523 timeout: Option<Duration>,
524) -> Result<(Tree, Option<Duration>), ParseError> {
525 let Some(timeout) = timeout.filter(|timeout| !timeout.is_zero()) else {
526 let tree = parser
527 .parse(text.as_bytes(), old_tree)
528 .ok_or_else(|| AdapterError::Parse("tree-sitter returned None".to_string()))?;
529 return Ok((tree, None));
530 };
531
532 let start = std::time::Instant::now();
533 let bytes = text.as_bytes();
534 let len = bytes.len();
535 let mut timed_out = false;
536 let tree = {
537 let mut input = |byte_offset, _| {
538 if byte_offset < len {
539 &bytes[byte_offset..]
540 } else {
541 &[]
542 }
543 };
544 let mut progress = |_: &tree_sitter::ParseState| {
545 if start.elapsed() >= timeout {
546 timed_out = true;
547 ControlFlow::Break(())
548 } else {
549 ControlFlow::Continue(())
550 }
551 };
552 let options = ParseOptions::new().progress_callback(&mut progress);
553 parser.parse_with_options(&mut input, old_tree, Some(options))
554 };
555 match tree {
556 Some(tree) => Ok((tree, None)),
557 None if timed_out => {
558 parser.reset();
559 let empty_tree = parser.parse("", None).ok_or_else(|| {
560 AdapterError::Parse("tree-sitter returned None after parse timeout".to_string())
561 })?;
562 Ok((empty_tree, Some(timeout)))
563 }
564 None => Err(AdapterError::Parse("tree-sitter returned None".to_string()).into()),
565 }
566}
567
568fn span_for_node(file: FileId, node: Node<'_>) -> bonsai_common::Span {
571 bonsai_common::Span::new(
572 file,
573 saturating_byte_offset(node.start_byte()),
574 saturating_byte_offset(node.end_byte()),
575 )
576}
577
578fn file_span(file: FileId, text_len: usize) -> bonsai_common::Span {
581 bonsai_common::Span::new(file, 0, saturating_byte_offset(text_len))
582}
583
584fn saturating_byte_offset(byte: usize) -> u64 {
585 u64::try_from(byte).unwrap_or(u64::MAX)
586}
587
588fn parse_timeout_diagnostic(file: FileId, text_len: usize, timeout: Duration) -> Diagnostic {
590 Diagnostic::new(
591 file_span(file, text_len),
592 Severity::Warning,
593 format!("file skipped: parse timeout after {} ms", timeout.as_millis()),
594 )
595 .with_code("parse-timeout")
596}
597
598#[cfg(test)]
599#[path = "tests.rs"]
600mod tests;