Skip to main content

dioxus_code/
advanced.rs

1//! Advanced building blocks for custom syntax-highlighted renderers.
2//!
3//! Most users should use [`Code()`](crate::Code()). This module exposes the
4//! lower-level source, span, segment, and theme helpers used by companion
5//! components such as `dioxus-code-editor`.
6//!
7//! ```rust
8//! use dioxus_code::Language;
9//! use dioxus_code::advanced::{HighlightSpan, HighlightedSource};
10//! static SPANS: &[HighlightSpan] = &[HighlightSpan::new(0..2, "k")];
11//! let src = HighlightedSource::from_static_parts("fn main() {}", Language::Rust, SPANS);
12//! assert_eq!(src.spans().len(), 1);
13//! ```
14
15use super::*;
16use std::{borrow::Cow, fmt, ops::Range};
17
18/// A typed error produced while preparing runtime syntax highlighting.
19///
20/// These errors are recoverable for renderers: callers can surface the error
21/// and render the original source as plaintext.
22#[derive(Debug, Clone, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum HighlightError {
25    /// The tree-sitter parser rejected the selected grammar.
26    GrammarLoad {
27        /// The language whose grammar failed to load.
28        language: Language,
29        /// The parser's diagnostic message.
30        message: String,
31    },
32    /// The tree-sitter highlights query failed to compile.
33    Query {
34        /// The language whose query failed.
35        language: Language,
36        /// Zero-based query row where the error was reported.
37        row: usize,
38        /// Zero-based query column where the error was reported.
39        column: usize,
40        /// Byte offset where the error was reported.
41        offset: usize,
42        /// The query error kind.
43        kind: HighlightQueryErrorKind,
44        /// The query compiler's diagnostic message.
45        message: String,
46    },
47    /// Tree-sitter failed to parse the source.
48    Parse {
49        /// The language being parsed.
50        language: Language,
51    },
52    /// [`Buffer::edit`] received offsets that are out of bounds or not on
53    /// UTF-8 character boundaries.
54    ///
55    /// The buffer's state is unchanged when this error is returned.
56    InvalidEdit {
57        /// First byte the caller said changed.
58        start_byte: usize,
59        /// One past the last byte of the replaced region in the previous source.
60        old_end_byte: usize,
61        /// One past the last byte of the inserted region in the new source.
62        new_end_byte: usize,
63        /// Length of the previous source, for context.
64        old_len: usize,
65        /// Length of the new source, for context.
66        new_len: usize,
67    },
68}
69
70impl HighlightError {
71    #[cfg(feature = "runtime")]
72    fn grammar_load(language: Language, error: arborium_tree_sitter::LanguageError) -> Self {
73        Self::GrammarLoad {
74            language,
75            message: error.to_string(),
76        }
77    }
78
79    #[cfg(feature = "runtime")]
80    fn query(language: Language, error: arborium_tree_sitter::QueryError) -> Self {
81        Self::Query {
82            language,
83            row: error.row,
84            column: error.column,
85            offset: error.offset,
86            kind: error.kind.into(),
87            message: error.message,
88        }
89    }
90}
91
92impl fmt::Display for HighlightError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::GrammarLoad { language, message } => {
96                write!(
97                    f,
98                    "failed to load grammar for {}: {}",
99                    language.slug(),
100                    message
101                )
102            }
103            Self::Query {
104                language,
105                row,
106                column,
107                kind,
108                message,
109                ..
110            } => {
111                write!(
112                    f,
113                    "query error for {} at {}:{} ({}): {}",
114                    language.slug(),
115                    row + 1,
116                    column + 1,
117                    kind,
118                    message
119                )
120            }
121            Self::Parse { language } => {
122                write!(f, "tree-sitter parse failed for {}", language.slug())
123            }
124            Self::InvalidEdit {
125                start_byte,
126                old_end_byte,
127                new_end_byte,
128                old_len,
129                new_len,
130            } => {
131                write!(
132                    f,
133                    "invalid edit: start_byte {start_byte}, old_end_byte {old_end_byte}, \
134                     new_end_byte {new_end_byte} (old_len {old_len}, new_len {new_len})"
135                )
136            }
137        }
138    }
139}
140
141impl std::error::Error for HighlightError {}
142
143/// The category of a tree-sitter highlights query compilation error.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145#[non_exhaustive]
146pub enum HighlightQueryErrorKind {
147    /// Invalid query syntax.
148    Syntax,
149    /// Invalid node type.
150    NodeType,
151    /// Invalid field name.
152    Field,
153    /// Invalid capture name.
154    Capture,
155    /// Invalid predicate.
156    Predicate,
157    /// Impossible query pattern structure.
158    Structure,
159    /// Query and grammar language mismatch.
160    Language,
161}
162
163impl fmt::Display for HighlightQueryErrorKind {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let kind = match self {
166            Self::Syntax => "syntax",
167            Self::NodeType => "node type",
168            Self::Field => "field",
169            Self::Capture => "capture",
170            Self::Predicate => "predicate",
171            Self::Structure => "structure",
172            Self::Language => "language",
173        };
174        f.write_str(kind)
175    }
176}
177
178#[cfg(feature = "runtime")]
179impl From<arborium_tree_sitter::QueryErrorKind> for HighlightQueryErrorKind {
180    fn from(kind: arborium_tree_sitter::QueryErrorKind) -> Self {
181        match kind {
182            arborium_tree_sitter::QueryErrorKind::Syntax => Self::Syntax,
183            arborium_tree_sitter::QueryErrorKind::NodeType => Self::NodeType,
184            arborium_tree_sitter::QueryErrorKind::Field => Self::Field,
185            arborium_tree_sitter::QueryErrorKind::Capture => Self::Capture,
186            arborium_tree_sitter::QueryErrorKind::Predicate => Self::Predicate,
187            arborium_tree_sitter::QueryErrorKind::Structure => Self::Structure,
188            arborium_tree_sitter::QueryErrorKind::Language => Self::Language,
189        }
190    }
191}
192
193/// A highlighted source string with metadata and token spans.
194///
195/// ```rust
196/// use dioxus_code::Language;
197/// use dioxus_code::advanced::HighlightedSource;
198/// let src = HighlightedSource::from_static_parts("let x = 1;", Language::Rust, &[]);
199/// assert_eq!(src.source(), "let x = 1;");
200/// ```
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct HighlightedSource {
203    source: Cow<'static, str>,
204    language: Language,
205    spans: Cow<'static, [HighlightSpan]>,
206}
207
208impl HighlightedSource {
209    #[cfg(feature = "runtime")]
210    pub(crate) fn from_owned_parts(
211        source: String,
212        language: Language,
213        spans: Vec<HighlightSpan>,
214    ) -> Self {
215        Self {
216            source: Cow::Owned(source),
217            language,
218            spans: Cow::Owned(spans),
219        }
220    }
221
222    /// Build highlighted source from static text and spans.
223    ///
224    /// This is mainly useful for compile-time highlighters and macro output.
225    ///
226    /// ```rust
227    /// use dioxus_code::Language;
228    /// use dioxus_code::advanced::HighlightedSource;
229    /// let src = HighlightedSource::from_static_parts("let x = 1;", Language::Rust, &[]);
230    /// assert_eq!(src.language(), Language::Rust);
231    /// ```
232    pub const fn from_static_parts(
233        source: &'static str,
234        language: Language,
235        spans: &'static [HighlightSpan],
236    ) -> Self {
237        Self {
238            source: Cow::Borrowed(source),
239            language,
240            spans: Cow::Borrowed(spans),
241        }
242    }
243
244    #[cfg(feature = "runtime")]
245    pub(crate) fn plaintext(source: impl Into<Cow<'static, str>>, language: Language) -> Self {
246        Self {
247            source: source.into(),
248            language,
249            spans: Cow::Borrowed(&[]),
250        }
251    }
252
253    /// The raw source text.
254    ///
255    /// ```rust
256    /// use dioxus_code::Language;
257    /// use dioxus_code::advanced::HighlightedSource;
258    /// let src = HighlightedSource::from_static_parts("hello", Language::Rust, &[]);
259    /// assert_eq!(src.source(), "hello");
260    /// ```
261    pub fn source(&self) -> &str {
262        self.source.as_ref()
263    }
264
265    /// The detected or explicitly set language, if any.
266    ///
267    /// ```rust
268    /// use dioxus_code::Language;
269    /// use dioxus_code::advanced::HighlightedSource;
270    /// let src = HighlightedSource::from_static_parts("", Language::Rust, &[]);
271    /// assert_eq!(src.language(), Language::Rust);
272    /// ```
273    pub const fn language(&self) -> Language {
274        self.language
275    }
276
277    /// The highlight spans covering the source.
278    ///
279    /// ```rust
280    /// use dioxus_code::Language;
281    /// use dioxus_code::advanced::HighlightedSource;
282    /// let src = HighlightedSource::from_static_parts("", Language::Rust, &[]);
283    /// assert!(src.spans().is_empty());
284    /// ```
285    pub fn spans(&self) -> &[HighlightSpan] {
286        self.spans.as_ref()
287    }
288
289    /// Split the source into renderable highlighted segments.
290    ///
291    /// ```rust
292    /// use dioxus_code::Language;
293    /// use dioxus_code::advanced::HighlightedSource;
294    /// let src = HighlightedSource::from_static_parts("hello", Language::Rust, &[]);
295    /// assert_eq!(src.segments().len(), 1);
296    /// ```
297    pub fn segments(&self) -> Vec<HighlightSegment<'_>> {
298        highlighted_segments(self.source(), self.spans())
299    }
300
301    pub(crate) fn trimmed_segments(&self) -> Vec<HighlightSegment<'_>> {
302        highlighted_segments(self.source().trim_end_matches('\n'), self.spans())
303    }
304
305    /// Split the source into highlighted lines.
306    ///
307    /// Trailing empty lines are preserved so editor renderers can keep line
308    /// numbers and input rows aligned with the original source text.
309    ///
310    /// ```rust
311    /// use dioxus_code::Language;
312    /// use dioxus_code::advanced::HighlightedSource;
313    /// let src = HighlightedSource::from_static_parts("a\nb", Language::Rust, &[]);
314    /// assert_eq!(src.lines().len(), 2);
315    /// ```
316    pub fn lines(&self) -> Vec<Vec<HighlightSegment<'_>>> {
317        highlighted_lines(self.source(), self.spans())
318    }
319}
320
321/// A highlight span attached to a byte range of source text.
322///
323/// ```rust
324/// use dioxus_code::advanced::HighlightSpan;
325/// let span = HighlightSpan::new(0..2, "k");
326/// assert_eq!(span.tag(), "k");
327/// ```
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub struct HighlightSpan {
330    start: u32,
331    end: u32,
332    tag: &'static str,
333}
334
335impl HighlightSpan {
336    /// Create a highlight span.
337    ///
338    /// ```rust
339    /// use dioxus_code::advanced::HighlightSpan;
340    /// let span = HighlightSpan::new(0..2, "k");
341    /// assert_eq!(span.range(), 0..2);
342    /// ```
343    pub const fn new(range: Range<u32>, tag: &'static str) -> Self {
344        Self {
345            start: range.start,
346            end: range.end,
347            tag,
348        }
349    }
350
351    /// Create a highlight span from explicit byte offsets.
352    ///
353    /// ```rust
354    /// use dioxus_code::advanced::HighlightSpan;
355    /// let span = HighlightSpan::from_offsets(0, 2, "k");
356    /// assert_eq!(span.start(), 0);
357    /// ```
358    pub const fn from_offsets(start: u32, end: u32, tag: &'static str) -> Self {
359        Self { start, end, tag }
360    }
361
362    /// Byte offset, inclusive, of the span's start in the source.
363    ///
364    /// ```rust
365    /// use dioxus_code::advanced::HighlightSpan;
366    /// assert_eq!(HighlightSpan::new(3..5, "k").start(), 3);
367    /// ```
368    pub const fn start(self) -> u32 {
369        self.start
370    }
371
372    /// Byte offset, exclusive, of the span's end in the source.
373    ///
374    /// ```rust
375    /// use dioxus_code::advanced::HighlightSpan;
376    /// assert_eq!(HighlightSpan::new(3..5, "k").end(), 5);
377    /// ```
378    pub const fn end(self) -> u32 {
379        self.end
380    }
381
382    /// Byte range covered by this span.
383    ///
384    /// ```rust
385    /// use dioxus_code::advanced::HighlightSpan;
386    /// assert_eq!(HighlightSpan::new(3..5, "k").range(), 3..5);
387    /// ```
388    pub const fn range(self) -> Range<u32> {
389        self.start..self.end
390    }
391
392    /// Highlight tag class suffix, for example `"k"` for keywords.
393    ///
394    /// ```rust
395    /// use dioxus_code::advanced::HighlightSpan;
396    /// assert_eq!(HighlightSpan::new(0..2, "k").tag(), "k");
397    /// ```
398    pub const fn tag(self) -> &'static str {
399        self.tag
400    }
401
402    #[cfg(feature = "runtime")]
403    pub(crate) fn set_end(&mut self, end: u32) {
404        self.end = end;
405    }
406}
407
408/// A borrowed render segment with an optional highlight tag.
409///
410/// ```rust
411/// use dioxus_code::advanced::HighlightSegment;
412/// let segment = HighlightSegment::new("fn", Some("k"));
413/// assert_eq!(segment.text(), "fn");
414/// ```
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct HighlightSegment<'a> {
417    text: &'a str,
418    tag: Option<&'static str>,
419}
420
421impl<'a> HighlightSegment<'a> {
422    /// Create a highlighted segment.
423    ///
424    /// ```rust
425    /// use dioxus_code::advanced::HighlightSegment;
426    /// let _segment = HighlightSegment::new("fn", Some("k"));
427    /// ```
428    pub const fn new(text: &'a str, tag: Option<&'static str>) -> Self {
429        Self { text, tag }
430    }
431
432    /// The source text for this segment.
433    ///
434    /// ```rust
435    /// use dioxus_code::advanced::HighlightSegment;
436    /// assert_eq!(HighlightSegment::new("fn", Some("k")).text(), "fn");
437    /// ```
438    pub const fn text(self) -> &'a str {
439        self.text
440    }
441
442    /// Highlight tag class suffix, when this segment is highlighted.
443    ///
444    /// ```rust
445    /// use dioxus_code::advanced::HighlightSegment;
446    /// assert_eq!(HighlightSegment::new("fn", Some("k")).tag(), Some("k"));
447    /// ```
448    pub const fn tag(self) -> Option<&'static str> {
449        self.tag
450    }
451}
452
453fn highlighted_segments<'a>(source: &'a str, spans: &[HighlightSpan]) -> Vec<HighlightSegment<'a>> {
454    if spans.is_empty() {
455        return vec![HighlightSegment::new(source, None)];
456    }
457
458    let mut spans = spans.to_vec();
459    spans.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| b.end.cmp(&a.end)));
460
461    let mut events = Vec::with_capacity(spans.len() * 2);
462    for (index, span) in spans.iter().enumerate() {
463        events.push((span.start, true, index));
464        events.push((span.end, false, index));
465    }
466    events.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
467
468    let mut segments = Vec::new();
469    let mut last_pos = 0;
470    let mut stack: Vec<usize> = Vec::new();
471
472    for (pos, is_start, span_index) in events {
473        let pos = pos as usize;
474        if pos > last_pos && pos <= source.len() {
475            segments.push(HighlightSegment::new(
476                &source[last_pos..pos],
477                stack.last().map(|&i| spans[i].tag),
478            ));
479            last_pos = pos;
480        }
481
482        if is_start {
483            stack.push(span_index);
484        } else if let Some(index) = stack.iter().rposition(|&i| i == span_index) {
485            stack.remove(index);
486        }
487    }
488
489    if last_pos < source.len() {
490        segments.push(HighlightSegment::new(
491            &source[last_pos..],
492            stack.last().map(|&i| spans[i].tag),
493        ));
494    }
495
496    segments
497}
498
499fn highlighted_lines<'a>(
500    source: &'a str,
501    spans: &[HighlightSpan],
502) -> Vec<Vec<HighlightSegment<'a>>> {
503    let mut lines = vec![Vec::new()];
504
505    for segment in highlighted_segments(source, spans) {
506        push_line_segments(&mut lines, segment);
507    }
508
509    lines
510}
511
512fn push_line_segments<'a>(
513    lines: &mut Vec<Vec<HighlightSegment<'a>>>,
514    segment: HighlightSegment<'a>,
515) {
516    let mut text = segment.text;
517
518    loop {
519        if let Some(newline) = text.find('\n') {
520            let before_newline = &text[..newline];
521            if !before_newline.is_empty() {
522                lines
523                    .last_mut()
524                    .unwrap()
525                    .push(HighlightSegment::new(before_newline, segment.tag));
526            }
527            lines.push(Vec::new());
528            text = &text[newline + 1..];
529        } else {
530            if !text.is_empty() {
531                lines
532                    .last_mut()
533                    .unwrap()
534                    .push(HighlightSegment::new(text, segment.tag));
535            }
536            break;
537        }
538    }
539}
540
541/// Props for [`TokenSpan`].
542///
543/// ```rust
544/// use dioxus_code::advanced::TokenSpanProps;
545/// let _props = TokenSpanProps { text: "fn".to_string(), tag: "k" };
546/// ```
547#[derive(Props, Clone, PartialEq)]
548pub struct TokenSpanProps {
549    /// The literal text rendered inside the span.
550    #[props(into)]
551    pub text: String,
552    /// Highlight tag class suffix used to derive the span's class name.
553    pub tag: &'static str,
554}
555
556/// Render one highlighted token as `<span class="a-{tag}">{text}</span>`.
557///
558/// ```rust
559/// use dioxus::prelude::*;
560/// use dioxus_code::advanced::TokenSpan;
561///
562/// fn _example() -> Element {
563///     rsx! { TokenSpan { text: "fn", tag: "k" } }
564/// }
565/// ```
566#[component]
567pub fn TokenSpan(props: TokenSpanProps) -> Element {
568    let class = format!("a-{}", props.tag);
569    rsx! {
570        span {
571            class,
572            "{props.text}"
573        }
574    }
575}
576
577/// A live `(text, grammar)` pair you can edit, reparse, and snapshot.
578///
579/// `Buffer` owns the source string, the parse tree, and the highlight spans as
580/// a single coherent unit. [`edit`](Self::edit) applies an incremental edit
581/// (reusing the cached parse tree); [`replace`](Self::replace) swaps the source
582/// wholesale; [`set_language`](Self::set_language) switches grammars and
583/// reparses. After any successful mutation, [`source`](Self::source),
584/// [`spans`](Self::spans), and [`lines`](Self::lines) reflect the new state.
585///
586/// Available with the `runtime` feature. Hold one per editor instance (e.g.
587/// inside `use_hook`). Highlighting operations return [`HighlightError`] when
588/// grammar setup or parsing fails.
589///
590/// ```rust
591/// use dioxus_code::Language;
592/// use dioxus_code::advanced::Buffer;
593/// let buffer = Buffer::new(Language::Rust, "fn main() {}").expect("rust grammar loads");
594/// assert_eq!(buffer.language(), Language::Rust);
595/// assert!(!buffer.spans().is_empty());
596/// ```
597#[cfg(feature = "runtime")]
598#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
599pub struct Buffer {
600    parser: arborium_tree_sitter::Parser,
601    cursor: arborium_tree_sitter::QueryCursor,
602    language: Language,
603    incremental: IncrementalGrammar,
604    tree: arborium_tree_sitter::Tree,
605    source: String,
606    spans: Vec<HighlightSpan>,
607}
608
609#[cfg(feature = "runtime")]
610struct IncrementalGrammar {
611    query: arborium_tree_sitter::Query,
612}
613
614#[cfg(feature = "runtime")]
615#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
616impl Buffer {
617    /// Create a buffer for `language`, parse `source`, and collect spans.
618    ///
619    /// ```rust
620    /// use dioxus_code::Language;
621    /// use dioxus_code::advanced::Buffer;
622    /// let buffer = Buffer::new(Language::Rust, "fn main() {}").expect("rust grammar loads");
623    /// assert_eq!(buffer.source(), "fn main() {}");
624    /// ```
625    pub fn new(language: Language, source: impl ToString) -> Result<Self, HighlightError> {
626        let source = source.to_string();
627        let (mut parser, incremental) = Self::parser_for(language)?;
628        let mut cursor = arborium_tree_sitter::QueryCursor::new();
629        let (tree, spans) = Self::parse_source(
630            language,
631            &mut parser,
632            &incremental.query,
633            &mut cursor,
634            &source,
635            None,
636        )?;
637
638        Ok(Self {
639            parser,
640            cursor,
641            language,
642            incremental,
643            tree,
644            source,
645            spans,
646        })
647    }
648
649    /// Replace the source wholesale and reparse from scratch.
650    ///
651    /// Use this when the new text is unrelated to the old (e.g. loading a
652    /// different file). For keystroke-level edits, prefer [`edit`](Self::edit)
653    /// — it reuses the cached parse tree.
654    ///
655    /// ```rust
656    /// use dioxus_code::Language;
657    /// use dioxus_code::advanced::Buffer;
658    /// let mut buffer = Buffer::new(Language::Rust, "fn old() {}").expect("rust grammar loads");
659    /// buffer.replace("fn new() {}").expect("rust parses");
660    /// assert_eq!(buffer.source(), "fn new() {}");
661    /// ```
662    pub fn replace(&mut self, source: impl ToString) -> Result<(), HighlightError> {
663        let source = source.to_string();
664        let (tree, spans) = Self::parse_source(
665            self.language,
666            &mut self.parser,
667            &self.incremental.query,
668            &mut self.cursor,
669            &source,
670            None,
671        )?;
672
673        self.source = source;
674        self.tree = tree;
675        self.spans = spans;
676        Ok(())
677    }
678
679    /// Apply an incremental edit and reparse, reusing the cached parse tree.
680    ///
681    /// `new_source` must be the full text *after* the edit. `edit` describes
682    /// the byte range that changed — its `start_byte` / `old_end_byte` index
683    /// into the buffer's previous source, and `new_end_byte` indexes into
684    /// `new_source`. If the edit is malformed, [`HighlightError::InvalidEdit`]
685    /// is returned and the buffer is left unchanged. Validation is limited to
686    /// bounds and UTF-8 character boundaries; callers are responsible for
687    /// passing an edit range that matches the unchanged prefix and suffix.
688    ///
689    /// ```rust
690    /// use dioxus_code::Language;
691    /// use dioxus_code::advanced::{Buffer, SourceEdit};
692    /// let mut buffer = Buffer::new(Language::Rust, "fn main() { 1 }").expect("rust grammar loads");
693    /// buffer.edit(
694    ///     SourceEdit { start_byte: 12, old_end_byte: 13, new_end_byte: 14 },
695    ///     "fn main() { 22 }",
696    /// ).expect("rust parses");
697    /// assert_eq!(buffer.source(), "fn main() { 22 }");
698    /// ```
699    pub fn edit(
700        &mut self,
701        edit: SourceEdit,
702        new_source: impl ToString,
703    ) -> Result<(), HighlightError> {
704        let new_source: String = new_source.to_string();
705        let input_edit = edit.into_input_edit(&self.source, &new_source)?;
706
707        let mut old_tree = self.tree.clone();
708        old_tree.edit(&input_edit);
709        let (tree, spans) = Self::parse_source(
710            self.language,
711            &mut self.parser,
712            &self.incremental.query,
713            &mut self.cursor,
714            &new_source,
715            Some(&old_tree),
716        )?;
717
718        self.source = new_source;
719        self.tree = tree;
720        self.spans = spans;
721        Ok(())
722    }
723
724    /// Switch grammars and reparse the current source.
725    ///
726    /// No-op when the new language matches the current one.
727    ///
728    /// ```rust
729    /// use dioxus_code::Language;
730    /// use dioxus_code::advanced::Buffer;
731    /// let mut buffer = Buffer::new(Language::Rust, "fn main() {}").expect("rust grammar loads");
732    /// buffer.set_language(Language::Rust).expect("rust grammar loads");
733    /// assert_eq!(buffer.language(), Language::Rust);
734    /// ```
735    pub fn set_language(&mut self, language: Language) -> Result<(), HighlightError> {
736        if self.language == language {
737            return Ok(());
738        }
739        let (mut parser, incremental) = Self::parser_for(language)?;
740        let (tree, spans) = Self::parse_source(
741            language,
742            &mut parser,
743            &incremental.query,
744            &mut self.cursor,
745            &self.source,
746            None,
747        )?;
748
749        self.parser = parser;
750        self.incremental = incremental;
751        self.language = language;
752        self.tree = tree;
753        self.spans = spans;
754        Ok(())
755    }
756
757    /// The current source text.
758    pub fn source(&self) -> &str {
759        &self.source
760    }
761
762    /// The grammar this buffer is parsing with.
763    pub const fn language(&self) -> Language {
764        self.language
765    }
766
767    /// Highlight spans covering the current source.
768    pub fn spans(&self) -> &[HighlightSpan] {
769        &self.spans
770    }
771
772    /// Split the source into renderable highlighted segments.
773    pub fn segments(&self) -> Vec<HighlightSegment<'_>> {
774        highlighted_segments(&self.source, &self.spans)
775    }
776
777    /// Split the source into highlighted lines, preserving trailing empty lines.
778    pub fn lines(&self) -> Vec<Vec<HighlightSegment<'_>>> {
779        highlighted_lines(&self.source, &self.spans)
780    }
781
782    /// Snapshot the buffer as an immutable [`HighlightedSource`].
783    ///
784    /// Useful for handing off to [`Code()`](crate::Code()) or any consumer
785    /// that takes the frozen snapshot type.
786    pub fn highlighted(&self) -> HighlightedSource {
787        HighlightedSource::from_owned_parts(self.source.clone(), self.language, self.spans.clone())
788    }
789
790    fn parser_for(
791        language: Language,
792    ) -> Result<(arborium_tree_sitter::Parser, IncrementalGrammar), HighlightError> {
793        let mut parser = arborium_tree_sitter::Parser::new();
794        let (language_fn, highlights_query) = grammar_for(language);
795        let ts_language: arborium_tree_sitter::Language = language_fn.into();
796        if let Err(error) = parser.set_language(&ts_language) {
797            return Err(HighlightError::grammar_load(language, error));
798        }
799
800        match arborium_tree_sitter::Query::new(&ts_language, highlights_query) {
801            Ok(query) => Ok((parser, IncrementalGrammar { query })),
802            Err(error) => Err(HighlightError::query(language, error)),
803        }
804    }
805
806    fn parse_source(
807        language: Language,
808        parser: &mut arborium_tree_sitter::Parser,
809        query: &arborium_tree_sitter::Query,
810        cursor: &mut arborium_tree_sitter::QueryCursor,
811        source: &str,
812        old_tree: Option<&arborium_tree_sitter::Tree>,
813    ) -> Result<(arborium_tree_sitter::Tree, Vec<HighlightSpan>), HighlightError> {
814        match parser.parse(source, old_tree) {
815            Some(tree) => {
816                let spans = collect_spans(query, cursor, &tree, source);
817                Ok((tree, spans))
818            }
819            None => Err(HighlightError::Parse { language }),
820        }
821    }
822}
823
824#[cfg(feature = "runtime")]
825fn collect_spans(
826    query: &arborium_tree_sitter::Query,
827    cursor: &mut arborium_tree_sitter::QueryCursor,
828    tree: &arborium_tree_sitter::Tree,
829    source: &str,
830) -> Vec<HighlightSpan> {
831    use arborium_tree_sitter::StreamingIterator;
832
833    let bytes = source.as_bytes();
834    let capture_names = query.capture_names();
835    let mut raw: Vec<RawHighlightSpan> = Vec::new();
836
837    let mut matches = cursor.matches(query, tree.root_node(), bytes);
838    while let Some(m) = matches.next() {
839        for capture in m.captures {
840            let name = capture_names[capture.index as usize];
841            if name.starts_with('_') || name.starts_with("injection.") {
842                continue;
843            }
844            raw.push(RawHighlightSpan {
845                start: capture.node.start_byte() as u32,
846                end: capture.node.end_byte() as u32,
847                tag: arborium_theme::tag_for_capture(name),
848                pattern_index: m.pattern_index as u32,
849            });
850        }
851    }
852
853    normalize_spans(raw)
854}
855
856#[cfg(feature = "runtime")]
857fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'static str) {
858    // Rust is bundled with the `runtime` feature; everything else is opt-in via
859    // its `lang-*` cargo feature (or the `all-languages` umbrella).
860    match language {
861        Language::Rust => (
862            arborium::lang_rust::language(),
863            arborium::lang_rust::HIGHLIGHTS_QUERY,
864        ),
865        #[cfg(feature = "lang-ada")]
866        Language::Ada => (
867            arborium::lang_ada::language(),
868            arborium::lang_ada::HIGHLIGHTS_QUERY,
869        ),
870        #[cfg(feature = "lang-agda")]
871        Language::Agda => (
872            arborium::lang_agda::language(),
873            arborium::lang_agda::HIGHLIGHTS_QUERY,
874        ),
875        #[cfg(feature = "lang-asciidoc")]
876        Language::Asciidoc => (
877            arborium::lang_asciidoc::language(),
878            arborium::lang_asciidoc::HIGHLIGHTS_QUERY,
879        ),
880        #[cfg(feature = "lang-asm")]
881        Language::Asm => (
882            arborium::lang_asm::language(),
883            arborium::lang_asm::HIGHLIGHTS_QUERY,
884        ),
885        #[cfg(feature = "lang-awk")]
886        Language::Awk => (
887            arborium::lang_awk::language(),
888            arborium::lang_awk::HIGHLIGHTS_QUERY,
889        ),
890        #[cfg(feature = "lang-bash")]
891        Language::Bash => (
892            arborium::lang_bash::language(),
893            arborium::lang_bash::HIGHLIGHTS_QUERY,
894        ),
895        #[cfg(feature = "lang-batch")]
896        Language::Batch => (
897            arborium::lang_batch::language(),
898            arborium::lang_batch::HIGHLIGHTS_QUERY,
899        ),
900        #[cfg(feature = "lang-c")]
901        Language::C => (
902            arborium::lang_c::language(),
903            arborium::lang_c::HIGHLIGHTS_QUERY,
904        ),
905        #[cfg(feature = "lang-c-sharp")]
906        Language::CSharp => (
907            arborium::lang_c_sharp::language(),
908            arborium::lang_c_sharp::HIGHLIGHTS_QUERY,
909        ),
910        #[cfg(feature = "lang-caddy")]
911        Language::Caddy => (
912            arborium::lang_caddy::language(),
913            arborium::lang_caddy::HIGHLIGHTS_QUERY,
914        ),
915        #[cfg(feature = "lang-capnp")]
916        Language::Capnp => (
917            arborium::lang_capnp::language(),
918            arborium::lang_capnp::HIGHLIGHTS_QUERY,
919        ),
920        #[cfg(feature = "lang-cedar")]
921        Language::Cedar => (
922            arborium::lang_cedar::language(),
923            arborium::lang_cedar::HIGHLIGHTS_QUERY,
924        ),
925        #[cfg(feature = "lang-cedarschema")]
926        Language::CedarSchema => (
927            arborium::lang_cedarschema::language(),
928            arborium::lang_cedarschema::HIGHLIGHTS_QUERY,
929        ),
930        #[cfg(feature = "lang-clojure")]
931        Language::Clojure => (
932            arborium::lang_clojure::language(),
933            arborium::lang_clojure::HIGHLIGHTS_QUERY,
934        ),
935        #[cfg(feature = "lang-cmake")]
936        Language::CMake => (
937            arborium::lang_cmake::language(),
938            arborium::lang_cmake::HIGHLIGHTS_QUERY,
939        ),
940        #[cfg(feature = "lang-cobol")]
941        Language::Cobol => (
942            arborium::lang_cobol::language(),
943            arborium::lang_cobol::HIGHLIGHTS_QUERY,
944        ),
945        #[cfg(feature = "lang-commonlisp")]
946        Language::CommonLisp => (
947            arborium::lang_commonlisp::language(),
948            arborium::lang_commonlisp::HIGHLIGHTS_QUERY,
949        ),
950        #[cfg(feature = "lang-cpp")]
951        Language::Cpp => (
952            arborium::lang_cpp::language(),
953            &arborium::lang_cpp::HIGHLIGHTS_QUERY,
954        ),
955        #[cfg(feature = "lang-css")]
956        Language::Css => (
957            arborium::lang_css::language(),
958            arborium::lang_css::HIGHLIGHTS_QUERY,
959        ),
960        #[cfg(feature = "lang-d")]
961        Language::D => (
962            arborium::lang_d::language(),
963            arborium::lang_d::HIGHLIGHTS_QUERY,
964        ),
965        #[cfg(feature = "lang-dart")]
966        Language::Dart => (
967            arborium::lang_dart::language(),
968            arborium::lang_dart::HIGHLIGHTS_QUERY,
969        ),
970        #[cfg(feature = "lang-devicetree")]
971        Language::DeviceTree => (
972            arborium::lang_devicetree::language(),
973            arborium::lang_devicetree::HIGHLIGHTS_QUERY,
974        ),
975        #[cfg(feature = "lang-diff")]
976        Language::Diff => (
977            arborium::lang_diff::language(),
978            arborium::lang_diff::HIGHLIGHTS_QUERY,
979        ),
980        #[cfg(feature = "lang-dockerfile")]
981        Language::Dockerfile => (
982            arborium::lang_dockerfile::language(),
983            arborium::lang_dockerfile::HIGHLIGHTS_QUERY,
984        ),
985        #[cfg(feature = "lang-dot")]
986        Language::Dot => (
987            arborium::lang_dot::language(),
988            arborium::lang_dot::HIGHLIGHTS_QUERY,
989        ),
990        #[cfg(feature = "lang-elisp")]
991        Language::Elisp => (
992            arborium::lang_elisp::language(),
993            arborium::lang_elisp::HIGHLIGHTS_QUERY,
994        ),
995        #[cfg(feature = "lang-elixir")]
996        Language::Elixir => (
997            arborium::lang_elixir::language(),
998            arborium::lang_elixir::HIGHLIGHTS_QUERY,
999        ),
1000        #[cfg(feature = "lang-elm")]
1001        Language::Elm => (
1002            arborium::lang_elm::language(),
1003            arborium::lang_elm::HIGHLIGHTS_QUERY,
1004        ),
1005        #[cfg(feature = "lang-erlang")]
1006        Language::Erlang => (
1007            arborium::lang_erlang::language(),
1008            arborium::lang_erlang::HIGHLIGHTS_QUERY,
1009        ),
1010        #[cfg(feature = "lang-fish")]
1011        Language::Fish => (
1012            arborium::lang_fish::language(),
1013            arborium::lang_fish::HIGHLIGHTS_QUERY,
1014        ),
1015        #[cfg(feature = "lang-fsharp")]
1016        Language::FSharp => (
1017            arborium::lang_fsharp::language(),
1018            arborium::lang_fsharp::HIGHLIGHTS_QUERY,
1019        ),
1020        #[cfg(feature = "lang-gleam")]
1021        Language::Gleam => (
1022            arborium::lang_gleam::language(),
1023            arborium::lang_gleam::HIGHLIGHTS_QUERY,
1024        ),
1025        #[cfg(feature = "lang-glsl")]
1026        Language::Glsl => (
1027            arborium::lang_glsl::language(),
1028            &arborium::lang_glsl::HIGHLIGHTS_QUERY,
1029        ),
1030        #[cfg(feature = "lang-go")]
1031        Language::Go => (
1032            arborium::lang_go::language(),
1033            arborium::lang_go::HIGHLIGHTS_QUERY,
1034        ),
1035        #[cfg(feature = "lang-graphql")]
1036        Language::GraphQL => (
1037            arborium::lang_graphql::language(),
1038            arborium::lang_graphql::HIGHLIGHTS_QUERY,
1039        ),
1040        #[cfg(feature = "lang-groovy")]
1041        Language::Groovy => (
1042            arborium::lang_groovy::language(),
1043            arborium::lang_groovy::HIGHLIGHTS_QUERY,
1044        ),
1045        #[cfg(feature = "lang-haskell")]
1046        Language::Haskell => (
1047            arborium::lang_haskell::language(),
1048            arborium::lang_haskell::HIGHLIGHTS_QUERY,
1049        ),
1050        #[cfg(feature = "lang-hcl")]
1051        Language::Hcl => (
1052            arborium::lang_hcl::language(),
1053            arborium::lang_hcl::HIGHLIGHTS_QUERY,
1054        ),
1055        #[cfg(feature = "lang-hlsl")]
1056        Language::Hlsl => (
1057            arborium::lang_hlsl::language(),
1058            &arborium::lang_hlsl::HIGHLIGHTS_QUERY,
1059        ),
1060        #[cfg(feature = "lang-html")]
1061        Language::Html => (
1062            arborium::lang_html::language(),
1063            arborium::lang_html::HIGHLIGHTS_QUERY,
1064        ),
1065        #[cfg(feature = "lang-idris")]
1066        Language::Idris => (
1067            arborium::lang_idris::language(),
1068            arborium::lang_idris::HIGHLIGHTS_QUERY,
1069        ),
1070        #[cfg(feature = "lang-ini")]
1071        Language::Ini => (
1072            arborium::lang_ini::language(),
1073            arborium::lang_ini::HIGHLIGHTS_QUERY,
1074        ),
1075        #[cfg(feature = "lang-java")]
1076        Language::Java => (
1077            arborium::lang_java::language(),
1078            arborium::lang_java::HIGHLIGHTS_QUERY,
1079        ),
1080        #[cfg(feature = "lang-javascript")]
1081        Language::JavaScript => (
1082            arborium::lang_javascript::language(),
1083            arborium::lang_javascript::HIGHLIGHTS_QUERY,
1084        ),
1085        #[cfg(feature = "lang-jinja2")]
1086        Language::Jinja2 => (
1087            arborium::lang_jinja2::language(),
1088            arborium::lang_jinja2::HIGHLIGHTS_QUERY,
1089        ),
1090        #[cfg(feature = "lang-jq")]
1091        Language::Jq => (
1092            arborium::lang_jq::language(),
1093            arborium::lang_jq::HIGHLIGHTS_QUERY,
1094        ),
1095        #[cfg(feature = "lang-json")]
1096        Language::Json => (
1097            arborium::lang_json::language(),
1098            arborium::lang_json::HIGHLIGHTS_QUERY,
1099        ),
1100        #[cfg(feature = "lang-julia")]
1101        Language::Julia => (
1102            arborium::lang_julia::language(),
1103            arborium::lang_julia::HIGHLIGHTS_QUERY,
1104        ),
1105        #[cfg(feature = "lang-kotlin")]
1106        Language::Kotlin => (
1107            arborium::lang_kotlin::language(),
1108            arborium::lang_kotlin::HIGHLIGHTS_QUERY,
1109        ),
1110        #[cfg(feature = "lang-lean")]
1111        Language::Lean => (
1112            arborium::lang_lean::language(),
1113            arborium::lang_lean::HIGHLIGHTS_QUERY,
1114        ),
1115        #[cfg(feature = "lang-lua")]
1116        Language::Lua => (
1117            arborium::lang_lua::language(),
1118            arborium::lang_lua::HIGHLIGHTS_QUERY,
1119        ),
1120        #[cfg(feature = "lang-markdown")]
1121        Language::Markdown => (
1122            arborium::lang_markdown::language(),
1123            arborium::lang_markdown::HIGHLIGHTS_QUERY,
1124        ),
1125        #[cfg(feature = "lang-matlab")]
1126        Language::Matlab => (
1127            arborium::lang_matlab::language(),
1128            arborium::lang_matlab::HIGHLIGHTS_QUERY,
1129        ),
1130        #[cfg(feature = "lang-meson")]
1131        Language::Meson => (
1132            arborium::lang_meson::language(),
1133            arborium::lang_meson::HIGHLIGHTS_QUERY,
1134        ),
1135        #[cfg(feature = "lang-nginx")]
1136        Language::Nginx => (
1137            arborium::lang_nginx::language(),
1138            arborium::lang_nginx::HIGHLIGHTS_QUERY,
1139        ),
1140        #[cfg(feature = "lang-ninja")]
1141        Language::Ninja => (
1142            arborium::lang_ninja::language(),
1143            arborium::lang_ninja::HIGHLIGHTS_QUERY,
1144        ),
1145        #[cfg(feature = "lang-nix")]
1146        Language::Nix => (
1147            arborium::lang_nix::language(),
1148            arborium::lang_nix::HIGHLIGHTS_QUERY,
1149        ),
1150        #[cfg(feature = "lang-objc")]
1151        Language::ObjectiveC => (
1152            arborium::lang_objc::language(),
1153            &arborium::lang_objc::HIGHLIGHTS_QUERY,
1154        ),
1155        #[cfg(feature = "lang-ocaml")]
1156        Language::OCaml => (
1157            arborium::lang_ocaml::language(),
1158            arborium::lang_ocaml::HIGHLIGHTS_QUERY,
1159        ),
1160        #[cfg(feature = "lang-perl")]
1161        Language::Perl => (
1162            arborium::lang_perl::language(),
1163            arborium::lang_perl::HIGHLIGHTS_QUERY,
1164        ),
1165        #[cfg(feature = "lang-php")]
1166        Language::Php => (
1167            arborium::lang_php::language(),
1168            arborium::lang_php::HIGHLIGHTS_QUERY,
1169        ),
1170        #[cfg(feature = "lang-postscript")]
1171        Language::PostScript => (
1172            arborium::lang_postscript::language(),
1173            arborium::lang_postscript::HIGHLIGHTS_QUERY,
1174        ),
1175        #[cfg(feature = "lang-powershell")]
1176        Language::PowerShell => (
1177            arborium::lang_powershell::language(),
1178            arborium::lang_powershell::HIGHLIGHTS_QUERY,
1179        ),
1180        #[cfg(feature = "lang-prolog")]
1181        Language::Prolog => (
1182            arborium::lang_prolog::language(),
1183            arborium::lang_prolog::HIGHLIGHTS_QUERY,
1184        ),
1185        #[cfg(feature = "lang-python")]
1186        Language::Python => (
1187            arborium::lang_python::language(),
1188            arborium::lang_python::HIGHLIGHTS_QUERY,
1189        ),
1190        #[cfg(feature = "lang-query")]
1191        Language::Query => (
1192            arborium::lang_query::language(),
1193            arborium::lang_query::HIGHLIGHTS_QUERY,
1194        ),
1195        #[cfg(feature = "lang-r")]
1196        Language::R => (
1197            arborium::lang_r::language(),
1198            arborium::lang_r::HIGHLIGHTS_QUERY,
1199        ),
1200        #[cfg(feature = "lang-rego")]
1201        Language::Rego => (
1202            arborium::lang_rego::language(),
1203            arborium::lang_rego::HIGHLIGHTS_QUERY,
1204        ),
1205        #[cfg(feature = "lang-rescript")]
1206        Language::Rescript => (
1207            arborium::lang_rescript::language(),
1208            arborium::lang_rescript::HIGHLIGHTS_QUERY,
1209        ),
1210        #[cfg(feature = "lang-ron")]
1211        Language::Ron => (
1212            arborium::lang_ron::language(),
1213            arborium::lang_ron::HIGHLIGHTS_QUERY,
1214        ),
1215        #[cfg(feature = "lang-ruby")]
1216        Language::Ruby => (
1217            arborium::lang_ruby::language(),
1218            arborium::lang_ruby::HIGHLIGHTS_QUERY,
1219        ),
1220        #[cfg(feature = "lang-scala")]
1221        Language::Scala => (
1222            arborium::lang_scala::language(),
1223            arborium::lang_scala::HIGHLIGHTS_QUERY,
1224        ),
1225        #[cfg(feature = "lang-scheme")]
1226        Language::Scheme => (
1227            arborium::lang_scheme::language(),
1228            arborium::lang_scheme::HIGHLIGHTS_QUERY,
1229        ),
1230        #[cfg(feature = "lang-scss")]
1231        Language::Scss => (
1232            arborium::lang_scss::language(),
1233            &arborium::lang_scss::HIGHLIGHTS_QUERY,
1234        ),
1235        #[cfg(feature = "lang-solidity")]
1236        Language::Solidity => (
1237            arborium::lang_solidity::language(),
1238            arborium::lang_solidity::HIGHLIGHTS_QUERY,
1239        ),
1240        #[cfg(feature = "lang-sparql")]
1241        Language::Sparql => (
1242            arborium::lang_sparql::language(),
1243            arborium::lang_sparql::HIGHLIGHTS_QUERY,
1244        ),
1245        #[cfg(feature = "lang-sql")]
1246        Language::Sql => (
1247            arborium::lang_sql::language(),
1248            arborium::lang_sql::HIGHLIGHTS_QUERY,
1249        ),
1250        #[cfg(feature = "lang-ssh-config")]
1251        Language::SshConfig => (
1252            arborium::lang_ssh_config::language(),
1253            arborium::lang_ssh_config::HIGHLIGHTS_QUERY,
1254        ),
1255        #[cfg(feature = "lang-starlark")]
1256        Language::Starlark => (
1257            arborium::lang_starlark::language(),
1258            arborium::lang_starlark::HIGHLIGHTS_QUERY,
1259        ),
1260        #[cfg(feature = "lang-styx")]
1261        Language::Styx => (
1262            arborium::lang_styx::language(),
1263            arborium::lang_styx::HIGHLIGHTS_QUERY,
1264        ),
1265        #[cfg(feature = "lang-svelte")]
1266        Language::Svelte => (
1267            arborium::lang_svelte::language(),
1268            &arborium::lang_svelte::HIGHLIGHTS_QUERY,
1269        ),
1270        #[cfg(feature = "lang-swift")]
1271        Language::Swift => (
1272            arborium::lang_swift::language(),
1273            arborium::lang_swift::HIGHLIGHTS_QUERY,
1274        ),
1275        #[cfg(feature = "lang-textproto")]
1276        Language::Textproto => (
1277            arborium::lang_textproto::language(),
1278            arborium::lang_textproto::HIGHLIGHTS_QUERY,
1279        ),
1280        #[cfg(feature = "lang-thrift")]
1281        Language::Thrift => (
1282            arborium::lang_thrift::language(),
1283            arborium::lang_thrift::HIGHLIGHTS_QUERY,
1284        ),
1285        #[cfg(feature = "lang-tlaplus")]
1286        Language::TlaPlus => (
1287            arborium::lang_tlaplus::language(),
1288            arborium::lang_tlaplus::HIGHLIGHTS_QUERY,
1289        ),
1290        #[cfg(feature = "lang-toml")]
1291        Language::Toml => (
1292            arborium::lang_toml::language(),
1293            arborium::lang_toml::HIGHLIGHTS_QUERY,
1294        ),
1295        #[cfg(feature = "lang-tsx")]
1296        Language::Tsx => (
1297            arborium::lang_tsx::language(),
1298            &arborium::lang_tsx::HIGHLIGHTS_QUERY,
1299        ),
1300        #[cfg(feature = "lang-typescript")]
1301        Language::TypeScript => (
1302            arborium::lang_typescript::language(),
1303            &arborium::lang_typescript::HIGHLIGHTS_QUERY,
1304        ),
1305        #[cfg(feature = "lang-typst")]
1306        Language::Typst => (
1307            arborium::lang_typst::language(),
1308            arborium::lang_typst::HIGHLIGHTS_QUERY,
1309        ),
1310        #[cfg(feature = "lang-uiua")]
1311        Language::Uiua => (
1312            arborium::lang_uiua::language(),
1313            arborium::lang_uiua::HIGHLIGHTS_QUERY,
1314        ),
1315        #[cfg(feature = "lang-vb")]
1316        Language::VisualBasic => (
1317            arborium::lang_vb::language(),
1318            arborium::lang_vb::HIGHLIGHTS_QUERY,
1319        ),
1320        #[cfg(feature = "lang-verilog")]
1321        Language::Verilog => (
1322            arborium::lang_verilog::language(),
1323            arborium::lang_verilog::HIGHLIGHTS_QUERY,
1324        ),
1325        #[cfg(feature = "lang-vhdl")]
1326        Language::Vhdl => (
1327            arborium::lang_vhdl::language(),
1328            arborium::lang_vhdl::HIGHLIGHTS_QUERY,
1329        ),
1330        #[cfg(feature = "lang-vim")]
1331        Language::Vim => (
1332            arborium::lang_vim::language(),
1333            arborium::lang_vim::HIGHLIGHTS_QUERY,
1334        ),
1335        #[cfg(feature = "lang-vue")]
1336        Language::Vue => (
1337            arborium::lang_vue::language(),
1338            &arborium::lang_vue::HIGHLIGHTS_QUERY,
1339        ),
1340        #[cfg(feature = "lang-wit")]
1341        Language::Wit => (
1342            arborium::lang_wit::language(),
1343            arborium::lang_wit::HIGHLIGHTS_QUERY,
1344        ),
1345        #[cfg(feature = "lang-x86asm")]
1346        Language::X86Asm => (
1347            arborium::lang_x86asm::language(),
1348            arborium::lang_x86asm::HIGHLIGHTS_QUERY,
1349        ),
1350        #[cfg(feature = "lang-xml")]
1351        Language::Xml => (
1352            arborium::lang_xml::language(),
1353            arborium::lang_xml::HIGHLIGHTS_QUERY,
1354        ),
1355        #[cfg(feature = "lang-yaml")]
1356        Language::Yaml => (
1357            arborium::lang_yaml::language(),
1358            arborium::lang_yaml::HIGHLIGHTS_QUERY,
1359        ),
1360        #[cfg(feature = "lang-yuri")]
1361        Language::Yuri => (
1362            arborium::lang_yuri::language(),
1363            arborium::lang_yuri::HIGHLIGHTS_QUERY,
1364        ),
1365        #[cfg(feature = "lang-zig")]
1366        Language::Zig => (
1367            arborium::lang_zig::language(),
1368            arborium::lang_zig::HIGHLIGHTS_QUERY,
1369        ),
1370        #[cfg(feature = "lang-zsh")]
1371        Language::Zsh => (
1372            arborium::lang_zsh::language(),
1373            arborium::lang_zsh::HIGHLIGHTS_QUERY,
1374        ),
1375    }
1376}
1377
1378/// A byte-range edit description used to drive incremental highlighting.
1379///
1380/// Build one from a real edit signal (for example a textarea `beforeinput`
1381/// event) and pass it to [`Buffer::edit`]. `start_byte` and
1382/// `old_end_byte` index into the buffer's previous source, while
1383/// `new_end_byte` indexes into the new source supplied alongside the edit.
1384///
1385/// ```rust
1386/// use dioxus_code::advanced::SourceEdit;
1387/// // Insertion of one byte at offset 0.
1388/// let _edit = SourceEdit { start_byte: 0, old_end_byte: 0, new_end_byte: 1 };
1389/// ```
1390#[cfg(feature = "runtime")]
1391#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
1392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1393pub struct SourceEdit {
1394    /// First byte that changed.
1395    pub start_byte: usize,
1396    /// One past the last byte of the replaced region in the previous source.
1397    pub old_end_byte: usize,
1398    /// One past the last byte of the inserted region in the new source.
1399    pub new_end_byte: usize,
1400}
1401
1402#[cfg(feature = "runtime")]
1403impl SourceEdit {
1404    fn into_input_edit(
1405        self,
1406        old_source: &str,
1407        new_source: &str,
1408    ) -> Result<arborium_tree_sitter::InputEdit, HighlightError> {
1409        if self.start_byte > self.old_end_byte
1410            || self.start_byte > self.new_end_byte
1411            || self.old_end_byte > old_source.len()
1412            || self.new_end_byte > new_source.len()
1413            || !old_source.is_char_boundary(self.start_byte)
1414            || !old_source.is_char_boundary(self.old_end_byte)
1415            || !new_source.is_char_boundary(self.start_byte)
1416            || !new_source.is_char_boundary(self.new_end_byte)
1417        {
1418            return Err(HighlightError::InvalidEdit {
1419                start_byte: self.start_byte,
1420                old_end_byte: self.old_end_byte,
1421                new_end_byte: self.new_end_byte,
1422                old_len: old_source.len(),
1423                new_len: new_source.len(),
1424            });
1425        }
1426
1427        Ok(arborium_tree_sitter::InputEdit {
1428            start_byte: self.start_byte,
1429            old_end_byte: self.old_end_byte,
1430            new_end_byte: self.new_end_byte,
1431            start_position: byte_to_point(old_source, self.start_byte),
1432            old_end_position: byte_to_point(old_source, self.old_end_byte),
1433            new_end_position: byte_to_point(new_source, self.new_end_byte),
1434        })
1435    }
1436}
1437
1438#[cfg(feature = "runtime")]
1439fn byte_to_point(text: &str, byte: usize) -> arborium_tree_sitter::Point {
1440    let prefix = &text.as_bytes()[..byte];
1441    let last_newline = prefix.iter().rposition(|&b| b == b'\n');
1442    let row = prefix.iter().filter(|&&b| b == b'\n').count();
1443    let column = match last_newline {
1444        Some(pos) => byte - pos - 1,
1445        None => byte,
1446    };
1447    arborium_tree_sitter::Point { row, column }
1448}
1449
1450/// Inject the shared syntax theme stylesheet and selected theme stylesheet.
1451///
1452/// ```rust
1453/// use dioxus::prelude::*;
1454/// use dioxus_code::{CodeTheme, Theme};
1455/// use dioxus_code::advanced::CodeThemeStyles;
1456///
1457/// fn _example() -> Element {
1458///     rsx! { CodeThemeStyles { theme: CodeTheme::fixed(Theme::TOKYO_NIGHT) } }
1459/// }
1460/// ```
1461#[component]
1462pub fn CodeThemeStyles(theme: CodeTheme) -> Element {
1463    let shared_theme_css = Theme::THEME_CSS;
1464
1465    match theme.stylesheets() {
1466        CodeThemeStylesheets::Fixed(stylesheet) => {
1467            let theme_asset = stylesheet.asset;
1468            let theme_key = stylesheet.class;
1469
1470            rsx! {
1471                document::Stylesheet { href: shared_theme_css }
1472                {rsx!{document::Stylesheet { key: "{theme_key}", href: theme_asset }}}
1473            }
1474        }
1475        CodeThemeStylesheets::System { light, dark } => {
1476            let light_asset = light.asset;
1477            let dark_asset = dark.asset;
1478            let light_key = light.class;
1479            let dark_key = dark.class;
1480
1481            rsx! {
1482                document::Stylesheet { href: shared_theme_css }
1483                {rsx!{document::Stylesheet { key: "{light_key}", href: light_asset }}}
1484                {rsx!{document::Stylesheet { key: "{dark_key}", href: dark_asset }}}
1485            }
1486        }
1487    }
1488}
1489
1490#[cfg(all(test, feature = "runtime"))]
1491mod buffer_tests {
1492    use super::*;
1493
1494    fn span_ranges(spans: &[HighlightSpan]) -> Vec<(u32, u32, &'static str)> {
1495        spans
1496            .iter()
1497            .map(|s| (s.start(), s.end(), s.tag()))
1498            .collect()
1499    }
1500
1501    fn batch_spans(source: &str, language: Language) -> Vec<HighlightSpan> {
1502        let snapshot: HighlightedSource = SourceCode::new(language, source.to_owned()).into();
1503        snapshot.spans().to_vec()
1504    }
1505
1506    #[test]
1507    fn new_matches_batch_path() {
1508        let source = "fn main() { let x = 1; }";
1509        let buffer = Buffer::new(Language::Rust, source).unwrap();
1510
1511        assert_eq!(buffer.language(), Language::Rust);
1512        assert_eq!(
1513            span_ranges(buffer.spans()),
1514            span_ranges(&batch_spans(source, Language::Rust)),
1515        );
1516    }
1517
1518    #[test]
1519    fn edit_with_explicit_source_edit() {
1520        let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap();
1521        let updated = "fn main() { let x = 12; }";
1522        buffer
1523            .edit(
1524                SourceEdit {
1525                    start_byte: 21,
1526                    old_end_byte: 21,
1527                    new_end_byte: 22,
1528                },
1529                updated,
1530            )
1531            .unwrap();
1532
1533        assert_eq!(buffer.source(), updated);
1534        assert_eq!(
1535            span_ranges(buffer.spans()),
1536            span_ranges(&batch_spans(updated, Language::Rust)),
1537        );
1538    }
1539
1540    #[test]
1541    fn malformed_edit_returns_typed_error_and_leaves_state_unchanged() {
1542        let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap();
1543        let previous_spans = buffer.spans().to_vec();
1544        let updated = "fn main() { let x = 12; }";
1545
1546        assert_eq!(
1547            buffer.edit(
1548                // old_end_byte beyond the previous source — must not panic.
1549                SourceEdit {
1550                    start_byte: 21,
1551                    old_end_byte: 999,
1552                    new_end_byte: 22,
1553                },
1554                updated,
1555            ),
1556            Err(HighlightError::InvalidEdit {
1557                start_byte: 21,
1558                old_end_byte: 999,
1559                new_end_byte: 22,
1560                old_len: "fn main() { let x = 1; }".len(),
1561                new_len: updated.len(),
1562            }),
1563        );
1564        assert_eq!(buffer.source(), "fn main() { let x = 1; }");
1565        assert_eq!(buffer.spans(), previous_spans.as_slice());
1566    }
1567
1568    #[test]
1569    fn semantic_edit_mismatch_is_not_validated() {
1570        let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap();
1571        let updated = "fn main() { let y = 1; }";
1572
1573        buffer
1574            .edit(
1575                // The unchanged suffix does not match this edit; validation
1576                // intentionally stays O(1) and trusts callers on semantics.
1577                SourceEdit {
1578                    start_byte: 21,
1579                    old_end_byte: 21,
1580                    new_end_byte: 22,
1581                },
1582                updated,
1583            )
1584            .unwrap();
1585
1586        assert_eq!(buffer.source(), updated);
1587    }
1588
1589    #[test]
1590    fn replace_drops_cached_tree() {
1591        let mut buffer = Buffer::new(Language::Rust, "fn main() { 1 }").unwrap();
1592        let updated = "fn main() { 2 }";
1593        buffer.replace(updated).unwrap();
1594
1595        assert_eq!(buffer.source(), updated);
1596        assert_eq!(
1597            span_ranges(buffer.spans()),
1598            span_ranges(&batch_spans(updated, Language::Rust)),
1599        );
1600    }
1601
1602    #[test]
1603    fn set_language_reparses() {
1604        let mut buffer = Buffer::new(Language::Rust, "fn main() {}").unwrap();
1605        let rust_spans = buffer.spans().to_vec();
1606
1607        // Re-set to the same language is a no-op but should still produce
1608        // the same output.
1609        buffer.set_language(Language::Rust).unwrap();
1610        assert_eq!(buffer.spans(), rust_spans.as_slice());
1611    }
1612
1613    #[test]
1614    fn byte_to_point_counts_rows_and_columns() {
1615        use arborium_tree_sitter::Point;
1616        assert_eq!(byte_to_point("abc", 2), Point { row: 0, column: 2 });
1617        assert_eq!(byte_to_point("ab\ncd\nef", 6), Point { row: 2, column: 0 });
1618        assert_eq!(byte_to_point("ab\ncd\nef", 8), Point { row: 2, column: 2 });
1619    }
1620}