Skip to main content

reovim_driver_syntax/
injection.rs

1//! Language injection types.
2//!
3//! This module defines types for representing embedded language regions
4//! within a source file, such as code blocks in markdown or script tags in HTML.
5
6use std::ops::Range;
7
8/// An injection point for embedded languages.
9///
10/// Injections allow one language to be embedded within another,
11/// such as code blocks in markdown or script tags in HTML.
12///
13/// # Single vs Combined Injections
14///
15/// - **Single-range**: A fenced code block in Markdown produces one injection
16///   with one byte range (e.g., the code block content).
17/// - **Combined**: Consecutive doc comment lines (`///`) produce one injection
18///   with multiple byte ranges (one per comment line), merged via
19///   `#set! injection.combined`.
20///
21/// # Example
22///
23/// ```
24/// use reovim_driver_syntax::Injection;
25///
26/// // A Rust code block in a markdown file
27/// let inj = Injection::new("rust", 100..200, 5, 3, 10, 3);
28/// assert_eq!(inj.language_id, "rust");
29/// assert!(inj.overlaps_lines(6, 8));
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Injection {
33    /// The language ID to use for the injected region.
34    pub language_id: String,
35    /// Byte ranges in the parent document.
36    /// Single-range injections (fenced code blocks) have exactly one element.
37    /// Combined injections (doc comment lines) have multiple elements.
38    pub ranges: Vec<Range<usize>>,
39    /// Start row (0-indexed) of the first range.
40    pub start_row: u32,
41    /// Start column (0-indexed) of the first range.
42    pub start_col: u32,
43    /// End row (0-indexed) of the last range.
44    pub end_row: u32,
45    /// End column (0-indexed) of the last range.
46    pub end_col: u32,
47}
48
49impl Injection {
50    /// Create a new single-range injection.
51    #[must_use]
52    pub fn new(
53        language_id: impl Into<String>,
54        byte_range: Range<usize>,
55        start_row: u32,
56        start_col: u32,
57        end_row: u32,
58        end_col: u32,
59    ) -> Self {
60        Self {
61            language_id: language_id.into(),
62            ranges: vec![byte_range],
63            start_row,
64            start_col,
65            end_row,
66            end_col,
67        }
68    }
69
70    /// Create a combined injection from multiple byte ranges.
71    ///
72    /// Used for `injection.combined` patterns where multiple source nodes
73    /// (e.g., consecutive doc comment lines) map to a single injection.
74    #[must_use]
75    pub fn combined(
76        language_id: impl Into<String>,
77        ranges: Vec<Range<usize>>,
78        start_row: u32,
79        start_col: u32,
80        end_row: u32,
81        end_col: u32,
82    ) -> Self {
83        Self {
84            language_id: language_id.into(),
85            ranges,
86            start_row,
87            start_col,
88            end_row,
89            end_col,
90        }
91    }
92
93    /// Create an injection from byte range only (row/col set to 0).
94    ///
95    /// Useful when only byte positions are known.
96    #[must_use]
97    pub fn from_bytes(language_id: impl Into<String>, byte_range: Range<usize>) -> Self {
98        Self {
99            language_id: language_id.into(),
100            ranges: vec![byte_range],
101            start_row: 0,
102            start_col: 0,
103            end_row: 0,
104            end_col: 0,
105        }
106    }
107
108    /// Get the overall byte range (start of first range to end of last range).
109    ///
110    /// For single-range injections, this is identical to the original range.
111    /// For combined injections, this spans the entire region.
112    #[must_use]
113    pub fn byte_range(&self) -> Range<usize> {
114        match (self.ranges.first(), self.ranges.last()) {
115            (Some(first), Some(last)) => first.start..last.end,
116            _ => 0..0,
117        }
118    }
119
120    /// Check if this injection overlaps with a line range.
121    ///
122    /// Both `start_line` and `end_line` are inclusive.
123    #[must_use]
124    pub const fn overlaps_lines(&self, start_line: u32, end_line: u32) -> bool {
125        self.start_row <= end_line && self.end_row >= start_line
126    }
127
128    /// Check if this injection contains a specific line.
129    #[must_use]
130    pub const fn contains_line(&self, line: u32) -> bool {
131        line >= self.start_row && line <= self.end_row
132    }
133
134    /// Check if this injection overlaps with a byte range.
135    #[must_use]
136    pub fn overlaps_bytes(&self, range: &Range<usize>) -> bool {
137        let overall = self.byte_range();
138        overall.start < range.end && overall.end > range.start
139    }
140
141    /// Get the total number of bytes across all ranges.
142    #[must_use]
143    pub fn byte_len(&self) -> usize {
144        self.ranges.iter().map(|r| r.end - r.start).sum()
145    }
146
147    /// Check if this injection spans multiple lines.
148    #[must_use]
149    pub const fn is_multiline(&self) -> bool {
150        self.end_row > self.start_row
151    }
152
153    /// Get the number of lines spanned by this injection.
154    #[must_use]
155    pub const fn line_count(&self) -> u32 {
156        self.end_row - self.start_row + 1
157    }
158
159    /// Check if this is a combined injection (multiple ranges).
160    #[must_use]
161    pub const fn is_combined(&self) -> bool {
162        self.ranges.len() > 1
163    }
164}
165
166#[cfg(test)]
167#[path = "injection_tests.rs"]
168mod tests;