// syntax.rs --- T M4.1 tree-sitter integration: parse types, the // per-buffer ParseView, and the worker-side run_parse function. // T M4.2 layers bundled grammars; T M4.3 adds highlight-query // loading and the capture-walk that feeds the highlight view. //! Tree-sitter integration (T M4.1 -- T M4.3). //! //! The async runtime ([`crate::async_runtime`]) already carries the //! dispatch shape Tree-sitter needs (parse-on-worker, supersede, //! frame-cadence settle). This module adds: //! //! * [`ParseRequest`] / [`ParseTreeBundle`] --- the inputs and outputs //! that travel between the main thread and a worker. //! * [`run_parse`] --- the worker-side body. Synchronous; called from //! the parse closure submitted by [`crate::async_runtime::AsyncRuntime::dispatch_parse`]. //! * [`ParseView`] / [`ParseViewHandle`] --- the per-buffer //! [`crate::view::View`] implementation that mirrors buffer bytes, //! captures every [`Edit`] as a [`tree_sitter::InputEdit`] (with //! correct row/col [`tree_sitter::Point`]s), and holds the most //! recent [`ParseTreeBundle`]. State lives behind an //! [`Arc>`] so the buffer-owned `Box` //! and the Lua-side glue (which needs to read the tree and feed //! back installed bundles) share the same backing store. //! * [`HighlightSpan`] / [`compute_highlight_spans`] --- T M4.3 //! capture-walk over a settled tree using a bundled //! `highlights.scm`. The resulting spans, sorted "wider first", //! feed [`crate::highlight::SyntaxHighlightView`]'s render path. //! //! M4.2 wires concrete grammars (`tree-sitter-rust`, //! `tree-sitter-lua`) on top of this module. M4.1 uses //! `tree-sitter-rust` only as a `dev-dependency` to drive acceptance //! tests. use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tree_sitter::StreamingIterator; use crate::async_runtime::JobId; use crate::buffer::{Buffer, BufferError, BufferId}; use crate::highlight::{Theme, ThemeHandle}; use crate::rope::Edit; use crate::view::View; /// Description of a parse job: the source bytes to parse, the /// language to parse against, the prior tree (if any) for incremental /// re-parse, and the [`tree_sitter::InputEdit`] descriptions /// accumulated since that prior tree was produced. /// /// All fields are owned ([R31]) so the closure submitted to a worker /// holds nothing borrowed from the main thread. #[derive(Clone, Debug)] pub struct ParseRequest { /// Bytes to parse. Materialized from the buffer's rope on the /// main thread before dispatch. pub source: Arc<[u8]>, /// Grammar language. `tree_sitter::Language` is a cheap /// pointer-to-static and `Send + Sync + Clone`. pub language: tree_sitter::Language, /// Human-readable language label, surfaced through Lua and the /// `*workers*` buffer ([T M3.7]). pub language_name: String, /// Tree from a prior parse of the same buffer, or `None` for a /// cold parse. The worker calls [`tree_sitter::Tree::edit`] for /// every entry in [`Self::edits`] before re-parsing. pub prior_tree: Option, /// Edits accumulated by the [`ParseView`] since `prior_tree` was /// produced. Empty for cold parses; non-empty drives incremental /// re-parse. pub edits: Vec, } /// Output of [`run_parse`]. The runtime's parse-handoff side map /// holds these by [`Arc`]; `Lua` introspection ([`crate::lua_bindings`]) /// resolves a buffer id to its current bundle and walks the tree. #[derive(Debug)] pub struct ParseTreeBundle { /// The freshly-produced parse tree. pub tree: tree_sitter::Tree, /// Source bytes the tree was parsed against. Co-owned with the /// request so node-byte-range lookups can read the underlying /// text (T M4.1 acceptance: "parse tree introspectable via Lua" /// implies the source the tree references). pub source: Arc<[u8]>, /// Language label. Stored alongside the tree so Lua can ask /// "what grammar produced this?". pub language_name: String, /// Wall-clock duration of the parse itself (excludes dispatch /// queueing, source materialization, and bus delivery). T M4.1 /// acceptance criteria are stated in this metric. pub parse_duration: Duration, } /// Run a parse. This is the worker-side body that the runtime's /// `dispatch_parse` closure invokes after pulling a job from the /// queue. Always synchronous --- there is no internal yielding. /// /// Returns `Err` if the language is rejected by [`tree_sitter::Parser`] /// (ABI mismatch, almost always a build issue) or if the parser /// itself returns no tree (cancellation flag flipped, exhausted /// timeout --- neither wired in M4.1, so under M4.1 contracts this /// path is unreachable in practice). pub fn run_parse(req: ParseRequest) -> Result { let mut parser = tree_sitter::Parser::new(); parser .set_language(&req.language) .map_err(|e| format!("set_language: {e}"))?; let mut prior = req.prior_tree; if let Some(tree) = prior.as_mut() { for edit in &req.edits { tree.edit(edit); } } let started = Instant::now(); let tree = parser .parse(req.source.as_ref(), prior.as_ref()) .ok_or_else(|| "parser produced no tree".to_owned())?; let parse_duration = started.elapsed(); Ok(ParseTreeBundle { tree, source: req.source, language_name: req.language_name, parse_duration, }) } /// Convert a byte offset within `source` to a tree-sitter /// `(row, column)` [`tree_sitter::Point`]. `byte` is clamped to /// `source.len()`. /// /// O(byte) on a linear scan. For 5000-line files (~150 KB) this is /// tens of microseconds per call --- well under the 5 ms incremental /// budget. A precomputed line-start index would be the obvious /// follow-up if profiling argues for it. #[must_use] pub fn byte_to_point(source: &[u8], byte: usize) -> tree_sitter::Point { let bounded = byte.min(source.len()); let mut row: usize = 0; let mut last_nl: Option = None; for (i, b) in source[..bounded].iter().enumerate() { if *b == b'\n' { row += 1; last_nl = Some(i); } } let column = match last_nl { Some(nl) => bounded - nl - 1, None => bounded, }; tree_sitter::Point::new(row, column) } /// Mutable state shared between the buffer-attached [`ParseView`] /// and any external [`ParseViewHandle`] clones. struct ParseViewInner { language: tree_sitter::Language, language_name: String, /// Source bytes mirror, kept in sync with the buffer. Updated /// inside `on_edit`. source: Vec, /// Edits accumulated since `current` was produced. Drained on /// `make_request`; cleared on `install`. pending: Vec, /// Most recent settled parse, or `None` if no parse has run yet. current: Option>, } /// Per-buffer parse-tree state. Attached to a [`Buffer`] as a /// [`View`]; `on_edit` mirrors the rope edit into a parallel /// `Vec` source buffer and pushes a corresponding /// [`tree_sitter::InputEdit`] onto a pending list. /// /// Internally a thin wrapper over `Arc>` so /// callers (Lua bindings, dispatch glue) can hold a /// [`ParseViewHandle`] clone and read/modify the same state without /// having to detach the view from the buffer. pub struct ParseView { inner: Arc>, } /// External handle to a [`ParseView`]'s state. Cheap to clone /// (`Arc` bump). Used by [`crate::lua_bindings`] to (a) build a /// [`ParseRequest`] before dispatch, (b) install the produced /// [`ParseTreeBundle`] after settle, (c) introspect the tree from /// Lua. #[derive(Clone)] pub struct ParseViewHandle { inner: Arc>, } impl ParseView { /// Construct a view by snapshotting the buffer's current bytes. /// The snapshot becomes the view's source mirror, so the first /// dispatched parse has byte-accurate input even before any edit /// is observed. #[must_use] pub fn new(buf: &Buffer, language: tree_sitter::Language, language_name: String) -> Self { let len = buf.len(); let mut source = Vec::with_capacity(len as usize); if len > 0 { for chunk in buf.snapshot_rope().chunks(0, len) { source.extend_from_slice(chunk); } } let inner = ParseViewInner { language, language_name, source, pending: Vec::new(), current: None, }; Self { inner: Arc::new(Mutex::new(inner)), } } /// Cheap clone of the shared state handle. #[must_use] pub fn handle(&self) -> ParseViewHandle { ParseViewHandle { inner: self.inner.clone(), } } } impl ParseViewHandle { /// Language this view parses against. #[must_use] pub fn language(&self) -> tree_sitter::Language { self.inner .lock() .expect("ParseView mutex poisoned") .language .clone() } /// Human-readable language label. #[must_use] pub fn language_name(&self) -> String { self.inner .lock() .expect("ParseView mutex poisoned") .language_name .clone() } /// Most recent parse, if any has settled. #[must_use] pub fn current(&self) -> Option> { self.inner .lock() .expect("ParseView mutex poisoned") .current .clone() } /// Number of pending edits waiting for the next parse dispatch. #[must_use] pub fn pending_edit_count(&self) -> usize { self.inner .lock() .expect("ParseView mutex poisoned") .pending .len() } /// Snapshot of the source mirror's current contents. Test /// helper; the worker receives the same bytes as `req.source` /// when a parse is dispatched. #[must_use] pub fn source_snapshot(&self) -> Vec { self.inner .lock() .expect("ParseView mutex poisoned") .source .clone() } /// Build a [`ParseRequest`] reflecting the current state. Drains /// the pending-edit list. Caller is expected to dispatch the /// request and feed the settled bundle back via [`Self::install`]. pub fn make_request(&self) -> ParseRequest { let mut inner = self.inner.lock().expect("ParseView mutex poisoned"); let edits = std::mem::take(&mut inner.pending); let prior_tree = inner.current.as_ref().map(|b| b.tree.clone()); ParseRequest { source: Arc::from(inner.source.clone()), language: inner.language.clone(), language_name: inner.language_name.clone(), prior_tree, edits, } } /// Install a freshly-parsed bundle. The caller is responsible /// for matching the bundle to the request that produced it --- /// installing a stale bundle would desynchronize the source /// mirror from the tree. pub fn install(&self, bundle: Arc) { self.inner.lock().expect("ParseView mutex poisoned").current = Some(bundle); } } /// One row of the bundled-grammar config (T M4.2). Adding a new /// grammar is a one-line addition to [`BUILTIN_LANGUAGES`] (plus the /// matching `tree-sitter-foo` line in `Cargo.toml`). /// /// The `loader` is a function pointer rather than a pre-materialized /// [`tree_sitter::Language`] so the C-side grammar object isn't /// touched until the first buffer of that language is opened --- /// "load grammar lazily" per the M4.2 acceptance criterion. The /// [`Self::highlights_query`] string is `include_str!`'d at compile /// time so it ships in the binary; T M4.3 compiles it into a /// [`tree_sitter::Query`] on first highlight attach. pub struct LanguageEntry { /// Canonical language name. Used by [`SyntaxRegistry::language`] /// lookups, surfaced through Lua as the grammar label. pub name: &'static str, /// File extensions (without the leading dot) that should auto- /// attach this grammar's [`ParseView`] when a file is opened. /// First match wins; ordering inside [`BUILTIN_LANGUAGES`] is /// the tiebreaker for ambiguous extensions. pub extensions: &'static [&'static str], /// Producer for the [`tree_sitter::Language`]. Called at most /// once per registry lifetime --- the result is cached under /// `name` after the first invocation. pub loader: fn() -> tree_sitter::Language, /// Source of the bundled `highlights.scm` query (T M4.3). Empty /// string means the grammar has no highlight query (rare; in /// that case the highlight view runs but emits nothing). pub highlights_query: &'static str, } /// Bundled grammars (T M4.2 + M4.3). The order is significant only /// for extensions that map to multiple languages --- none of the /// v0.1 entries collide. /// /// Adding a grammar: /// 1. Add `tree-sitter-foo = "X.Y"` to `Cargo.toml`. /// 2. Add one [`LanguageEntry`] here, including /// `tree_sitter_foo::HIGHLIGHTS_QUERY`. /// 3. (Done.) The Lua side picks up the new grammar through the /// `buffer.after-load` hook automatically and the highlight /// overlay attaches in the same step. pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[ LanguageEntry { name: "rust", extensions: &["rs"], loader: || tree_sitter_rust::LANGUAGE.into(), highlights_query: tree_sitter_rust::HIGHLIGHTS_QUERY, }, LanguageEntry { name: "lua", extensions: &["lua"], loader: || tree_sitter_lua::LANGUAGE.into(), highlights_query: tree_sitter_lua::HIGHLIGHTS_QUERY, }, // T M9.7: markdown grammar for prompt-result buffers with // `_meta.format = "markdown"`. Uses only the block grammar // (`tree_sitter_md::LANGUAGE`) — block-level highlighting (headers, // lists, fenced code blocks, blockquotes) is the v0.1 floor. // Inline highlighting (emphasis, links inside running text) would // require the dual-grammar `MarkdownParser` and is M9.8+ work. // The `markdown_inline` fixture prompt + matching acceptance test // pin this floor: an `**emphasis**` span must not crash, and is // expected to render unhighlighted; any future expansion that // adds inline coverage is additive, not a regression. // Note the constant name: `HIGHLIGHT_QUERY_BLOCK` (singular) is // the markdown crate's idiom; `tree-sitter-rust` and // `tree-sitter-lua` use `HIGHLIGHTS_QUERY` (plural). LanguageEntry { name: "markdown", extensions: &["md", "markdown"], loader: || tree_sitter_md::LANGUAGE.into(), highlights_query: tree_sitter_md::HIGHLIGHT_QUERY_BLOCK, }, ]; /// Registry that the Lua surface ([`crate::lua_bindings::install_parse`]) /// reads to map language names to grammars and buffer ids to attached /// [`ParseView`] handles. Held by `Rc<...>` --- main-thread state, no /// cross-thread sharing. pub struct SyntaxRegistry { languages: RefCell>, views: RefCell>, /// Job id → buffer id mapping populated when a parse is dispatched /// for a buffer. The Lua-side install path looks up the buffer id /// from the settled job's id so it can drain the parse-handoff /// bundle into the right view. parse_jobs: RefCell>, /// Custom (non-builtin) `extension → language name` mappings /// registered at runtime. Hosts that want to wire an extra /// grammar without touching [`BUILTIN_LANGUAGES`] can call /// [`SyntaxRegistry::register_extension`] alongside /// [`SyntaxRegistry::register_language`]. Looked up *after* /// [`BUILTIN_LANGUAGES`] so users can't accidentally shadow a /// builtin extension. extra_extensions: RefCell>, /// Compiled `highlights.scm` query per language (T M4.3). Lazy- /// compiled from [`LanguageEntry::highlights_query`] on first /// access; cached for the registry's lifetime. The result of a /// compilation failure (e.g. grammar / query ABI skew) is /// cached as `Err(message)` so we don't burn cycles re-trying. queries: RefCell, String>>>, /// Active theme (T M4.3). Shared with every /// [`crate::highlight::SyntaxHighlightView`] attached through /// this registry --- editing the theme through Lua updates all /// attached views in lock-step. theme: ThemeHandle, } /// Cheaply-cloneable shared handle to a [`SyntaxRegistry`]. Same /// `Rc>` shape as the other shared registries in /// [`crate::lua_bindings`]. pub type SharedSyntaxRegistry = Rc; impl SyntaxRegistry { /// Construct an empty registry. Languages are registered by the /// host (Rust startup) before Lua scripts run, or lazy-loaded /// from [`BUILTIN_LANGUAGES`] on first lookup. Theme starts at /// the [`Theme::default_dark`] palette so an opening rust file /// gets a usable highlight without any Lua configuration. #[must_use] pub fn new() -> Self { Self { languages: RefCell::new(HashMap::new()), views: RefCell::new(HashMap::new()), parse_jobs: RefCell::new(HashMap::new()), extra_extensions: RefCell::new(HashMap::new()), queries: RefCell::new(HashMap::new()), theme: Arc::new(Mutex::new(Theme::default_dark())), } } /// Shared theme handle. Cheap clone (an [`Arc`] bump). T M4.3: /// every [`crate::highlight::SyntaxHighlightView`] holds a clone /// of this same `Arc>`, so a Lua-driven theme edit /// is observable on the next render. #[must_use] pub fn theme(&self) -> ThemeHandle { self.theme.clone() } /// Register a tree-sitter [`tree_sitter::Language`] under `name`. /// Subsequent registrations under the same name overwrite the /// prior entry. Called from Rust startup; Lua scripts cannot /// construct `tree_sitter::Language` values directly. pub fn register_language(&self, name: impl Into, lang: tree_sitter::Language) { self.languages.borrow_mut().insert(name.into(), lang); } /// Register a runtime extension → language mapping. The name /// must already exist (either from a manual /// [`Self::register_language`] or in [`BUILTIN_LANGUAGES`]). /// Custom mappings sit *after* [`BUILTIN_LANGUAGES`] in the /// lookup order, so users can't accidentally shadow a builtin. /// T M4.2: lets a host wire an out-of-tree grammar without /// touching the builtin table. pub fn register_extension(&self, ext: impl Into, lang_name: impl Into) { self.extra_extensions .borrow_mut() .insert(ext.into(), lang_name.into()); } /// Look up a language by name. On miss, consults /// [`BUILTIN_LANGUAGES`] and lazy-loads the entry on demand /// (caching it for the rest of the registry's lifetime). #[must_use] pub fn language(&self, name: &str) -> Option { if let Some(lang) = self.languages.borrow().get(name).cloned() { return Some(lang); } let entry = BUILTIN_LANGUAGES.iter().find(|e| e.name == name)?; let lang = (entry.loader)(); self.languages .borrow_mut() .insert(name.to_owned(), lang.clone()); Some(lang) } /// True if `name` is a registered or builtin language. #[must_use] pub fn has_language(&self, name: &str) -> bool { self.languages.borrow().contains_key(name) || BUILTIN_LANGUAGES.iter().any(|e| e.name == name) } /// Resolve a file extension to a language name. Checks /// [`BUILTIN_LANGUAGES`] first, then runtime-registered extras. /// Match is case-sensitive (file extensions traditionally are); /// extension is the part *after* the last `.`, with no leading /// dot. T M4.2. #[must_use] pub fn language_name_for_extension(&self, ext: &str) -> Option<&'static str> { BUILTIN_LANGUAGES .iter() .find(|e| e.extensions.contains(&ext)) .map(|e| e.name) } /// Resolve a file path to a language name. Strips the path to /// its extension and delegates to /// [`Self::language_name_for_extension`]. Returns `None` for /// extensionless paths and unrecognized extensions. #[must_use] pub fn language_name_for_path(&self, path: &str) -> Option { let ext = std::path::Path::new(path) .extension() .and_then(|os| os.to_str())?; if let Some(name) = self.language_name_for_extension(ext) { return Some(name.to_owned()); } self.extra_extensions.borrow().get(ext).cloned() } /// Record the [`ParseViewHandle`] attached to a buffer. pub fn attach_view(&self, buffer: BufferId, handle: ParseViewHandle) { self.views.borrow_mut().insert(buffer, handle); } /// Retrieve the handle for `buffer` if one is attached. #[must_use] pub fn view(&self, buffer: BufferId) -> Option { self.views.borrow().get(&buffer).cloned() } /// Forget the handle for `buffer`. Called when a buffer is /// removed. pub fn detach_view(&self, buffer: BufferId) { self.views.borrow_mut().remove(&buffer); } /// Record that `job_id` was dispatched for `buffer`. The settle /// path looks the buffer up from the job id so it can install the /// settled bundle into the right view. pub fn record_parse_job(&self, job_id: JobId, buffer: BufferId) { self.parse_jobs.borrow_mut().insert(job_id, buffer); } /// Drain the recorded buffer-id for `job_id`. #[must_use] pub fn take_parse_job(&self, job_id: JobId) -> Option { self.parse_jobs.borrow_mut().remove(&job_id) } /// Number of unsettled parse-job → buffer mappings. Test helper. #[must_use] pub fn pending_parse_job_count(&self) -> usize { self.parse_jobs.borrow().len() } /// Lazy-compile and cache the bundled `highlights.scm` query for /// `lang_name`. Returns `None` if the language is unknown, the /// language entry has an empty query (no highlights shipped), /// or compilation failed (the failure is cached so subsequent /// calls don't re-attempt). T M4.3. #[must_use] pub fn highlights_query(&self, lang_name: &str) -> Option> { if let Some(slot) = self.queries.borrow().get(lang_name) { return slot.as_ref().ok().cloned(); } let language = self.language(lang_name)?; let entry = BUILTIN_LANGUAGES.iter().find(|e| e.name == lang_name); let source = entry.map_or("", |e| e.highlights_query); if source.is_empty() { self.queries .borrow_mut() .insert(lang_name.to_owned(), Err("no highlights query".to_owned())); return None; } let compiled = tree_sitter::Query::new(&language, source) .map(Arc::new) .map_err(|e| format!("compile {lang_name} highlights: {e:?}")); let result = compiled.as_ref().ok().cloned(); self.queries .borrow_mut() .insert(lang_name.to_owned(), compiled); result } } impl Default for SyntaxRegistry { fn default() -> Self { Self::new() } } impl View for ParseView { fn on_edit(&mut self, _buf: &Buffer, edit: &Edit) -> Result<(), BufferError> { let start_byte = edit.range.start as usize; let old_end_byte = edit.range.end as usize; let new_end_byte = start_byte + edit.inserted_len as usize; let mut inner = self.inner.lock().expect("ParseView mutex poisoned"); // Compute pre-edit Points BEFORE mutating the source mirror. let start_position = byte_to_point(&inner.source, start_byte); let old_end_position = byte_to_point(&inner.source, old_end_byte); // Splice: replace [start..old_end] with the inserted bytes // pulled from the new rope. The inserted bytes are at // [start_byte, new_end_byte) in the new rope. let mut new_bytes = vec![0u8; edit.inserted_len as usize]; if !new_bytes.is_empty() { edit.new_rope .slice(start_byte as u64, new_end_byte as u64, &mut new_bytes); } inner.source.splice(start_byte..old_end_byte, new_bytes); // Now compute the new_end Point against the updated source. let new_end_position = byte_to_point(&inner.source, new_end_byte); inner.pending.push(tree_sitter::InputEdit { start_byte, old_end_byte, new_end_byte, start_position, old_end_position, new_end_position, }); Ok(()) } } // --------------------------------------------------------------------------- // T M4.3: highlight-span extraction // --------------------------------------------------------------------------- /// One highlighted byte range produced by a `highlights.scm` query /// run against a [`ParseTreeBundle`]. The `capture_index` is into /// [`tree_sitter::Query::capture_names`]; resolving to a style /// happens inside [`crate::highlight::SyntaxHighlightView`] using /// the active theme. /// /// `start_byte`/`end_byte` are byte offsets into the bundle's /// `source`. Stored as `u32` because pmacs files cap at 4 GiB /// (rope domain) and downstream coordinates (rows, cols) are also /// `u32` --- avoids signed/unsigned conversion noise on the render /// hot path. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct HighlightSpan { /// First byte covered by the span. pub start_byte: u32, /// One past the last byte covered. pub end_byte: u32, /// Index into [`tree_sitter::Query::capture_names`] for the /// capture that produced this span. pub capture_index: u32, } /// Walk every capture produced by `query` over `bundle.tree`, /// collect them as [`HighlightSpan`]s, and sort them so that wider /// (outer) ranges come *before* narrower (inner) ones at the same /// start byte. The render path then applies them in order, so inner /// captures end up overriding outer ones --- matches typical /// editor highlight precedence ("the most specific node wins"). /// /// O(captures · log captures) for the sort; O(query work) for the /// capture walk itself (the dominant cost; see M4.3 acceptance). #[must_use] pub fn compute_highlight_spans( query: &tree_sitter::Query, bundle: &ParseTreeBundle, ) -> Vec { let mut spans = Vec::new(); let mut cursor = tree_sitter::QueryCursor::new(); let source: &[u8] = bundle.source.as_ref(); let root = bundle.tree.root_node(); let mut iter = cursor.captures(query, root, source); while let Some((qmatch, capture_idx)) = iter.next() { let cap = qmatch.captures[*capture_idx]; spans.push(HighlightSpan { start_byte: cap.node.start_byte() as u32, end_byte: cap.node.end_byte() as u32, capture_index: cap.index, }); } // Wider-first ordering at equal start: later writes (the // narrower / more specific spans) override earlier ones in the // overlay merge. spans.sort_by(|a, b| { a.start_byte .cmp(&b.start_byte) .then_with(|| b.end_byte.cmp(&a.end_byte)) .then_with(|| a.capture_index.cmp(&b.capture_index)) }); spans } #[cfg(test)] mod tests { use super::*; use crate::buffer::{Buffer, BufferId, EditOp}; fn fresh_buffer(name: &str) -> Buffer { Buffer::new(BufferId::next(), name) } fn rust_view(buf: &Buffer) -> (ParseView, ParseViewHandle) { let view = ParseView::new(buf, tree_sitter_rust::LANGUAGE.into(), "rust".to_owned()); let handle = view.handle(); (view, handle) } fn parse_synchronously(handle: &ParseViewHandle) -> Arc { let req = handle.make_request(); let bundle = Arc::new(run_parse(req).expect("parse succeeds")); handle.install(bundle.clone()); bundle } #[test] fn byte_to_point_handles_first_line() { let src = b"hello world"; assert_eq!(byte_to_point(src, 0), tree_sitter::Point::new(0, 0)); assert_eq!(byte_to_point(src, 5), tree_sitter::Point::new(0, 5)); assert_eq!(byte_to_point(src, 11), tree_sitter::Point::new(0, 11)); } #[test] fn byte_to_point_counts_newlines() { let src = b"a\nbb\nccc"; assert_eq!(byte_to_point(src, 0), tree_sitter::Point::new(0, 0)); assert_eq!(byte_to_point(src, 1), tree_sitter::Point::new(0, 1)); assert_eq!(byte_to_point(src, 2), tree_sitter::Point::new(1, 0)); assert_eq!(byte_to_point(src, 4), tree_sitter::Point::new(1, 2)); assert_eq!(byte_to_point(src, 5), tree_sitter::Point::new(2, 0)); assert_eq!(byte_to_point(src, 8), tree_sitter::Point::new(2, 3)); } #[test] fn byte_to_point_clamps_past_end() { let src = b"abc\ndef"; assert_eq!(byte_to_point(src, 999), byte_to_point(src, src.len())); } #[test] fn parse_view_records_insert_input_edit() { let mut buf = fresh_buffer("scratch.rs"); buf.apply_edit(EditOp::Insert { pos: 0, bytes: b"fn main() {}\n", }) .unwrap(); let (view, handle) = rust_view(&buf); let _vid = buf.attach_view(Box::new(view)); // Insert " let x = 1;" between `{` and `}`. Position 11 is // the byte after `{` in "fn main() {}\n". buf.apply_edit(EditOp::Insert { pos: 11, bytes: b" let x = 1;", }) .unwrap(); assert_eq!(handle.pending_edit_count(), 1); assert_eq!(handle.source_snapshot(), b"fn main() { let x = 1;}\n"); } #[test] fn parse_view_round_trips_initial_then_incremental_parse() { let mut buf = fresh_buffer("scratch.rs"); buf.apply_edit(EditOp::Insert { pos: 0, bytes: b"fn main() {}\n", }) .unwrap(); let (view, handle) = rust_view(&buf); let _vid = buf.attach_view(Box::new(view)); // Cold parse. let bundle = parse_synchronously(&handle); assert_eq!(bundle.tree.root_node().kind(), "source_file"); assert_eq!(handle.pending_edit_count(), 0); // One incremental edit, then re-parse with the new source + // accumulated InputEdits. buf.apply_edit(EditOp::Insert { pos: 11, bytes: b" let _ = 1;", }) .unwrap(); assert_eq!(handle.pending_edit_count(), 1); let bundle = parse_synchronously(&handle); assert_eq!(bundle.tree.root_node().kind(), "source_file"); assert_eq!( bundle.source.as_ref(), b"fn main() { let _ = 1;}\n", "bundle source must reflect post-edit bytes" ); // Pending list cleared on make_request. assert_eq!(handle.pending_edit_count(), 0); } }