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;