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