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
use super::{Extent, Snippet, SnippetError};
use crate::Rope;
use super::boundary::{
calculate_bytes_extent, calculate_chars_extent, calculate_lines_extent,
calculate_matching_extent, BoundaryMode,
};
#[derive(Debug, Clone, PartialEq, Eq)]
/// The concrete start and end indices of a resolved snippet within a [`Rope`].
pub struct SnippetResolution {
/// The starting character index of the resolved snippet.
pub start: usize,
/// The ending character index of the resolved snippet (exclusive).
pub end: usize,
}
/// Validates that a range is valid and within rope bounds.
fn validate_range(start: usize, end: usize, rope: &Rope) -> Result<(), SnippetError> {
let rope_len = rope.len_chars();
// Allow start == end for zero-width ranges (insertions)
if start > end {
return Err(SnippetError::InvalidRange { start, end });
}
if end > rope_len {
return Err(SnippetError::OutOfBounds {
index: end,
rope_len,
});
}
Ok(())
}
impl Snippet {
/// Resolves this snippet into absolute character indices.
///
/// # Errors
///
/// Returns [`SnippetError`] if boundaries cannot be resolved or the resulting range is invalid.
pub fn resolve(&self, rope: &Rope) -> Result<SnippetResolution, SnippetError> {
match self {
Snippet::At(boundary) => {
let res = boundary.resolve(rope)?;
validate_range(res.start, res.end, rope)?;
Ok(SnippetResolution {
start: res.start,
end: res.end,
})
}
Snippet::From(boundary) => {
let res = boundary.resolve(rope)?;
let end = rope.len_chars();
validate_range(res.end, end, rope)?;
Ok(SnippetResolution {
start: res.end,
end,
})
}
Snippet::To(boundary) => {
let (target_start, target_end) = boundary.target.resolve_range(rope)?;
let to_end = match &boundary.mode {
BoundaryMode::Exclude => target_start, // Before the target
BoundaryMode::Include => target_end, // After the target
BoundaryMode::Extend(extent) => match extent {
Extent::Lines(n) => calculate_lines_extent(rope, target_end, *n)?,
Extent::Chars(n) => calculate_chars_extent(rope, target_end, *n)?,
Extent::Bytes(n) => calculate_bytes_extent(rope, target_end, *n)?,
Extent::Matching(n, t) => {
calculate_matching_extent(rope, target_end, *n, t)?
}
},
};
validate_range(0, to_end, rope)?;
Ok(SnippetResolution {
start: 0,
end: to_end,
})
}
Snippet::Between { start, end } => {
// For Between semantics:
// - Start boundary in Exclude mode: start AFTER the target (use .end)
// - Start boundary in Include mode: start AT the target (use .start)
// - End boundary in Exclude mode: end BEFORE the target (need target.start)
// - End boundary in Include mode: end AFTER the target (use .end)
let start_res = start.resolve(rope)?;
let (end_target_start, end_target_end) = end.target.resolve_range(rope)?;
let between_start = start_res.start;
let between_end = match &end.mode {
BoundaryMode::Exclude => end_target_start, // Before the target
BoundaryMode::Include => end_target_end, // After the target
BoundaryMode::Extend(extent) => {
// Extend mode: start from end of target and extend
match extent {
Extent::Lines(n) => calculate_lines_extent(rope, end_target_end, *n)?,
Extent::Chars(n) => calculate_chars_extent(rope, end_target_end, *n)?,
Extent::Bytes(n) => calculate_bytes_extent(rope, end_target_end, *n)?,
Extent::Matching(n, t) => {
calculate_matching_extent(rope, end_target_end, *n, t)?
}
}
}
};
validate_range(between_start, between_end, rope)?;
Ok(SnippetResolution {
start: between_start,
end: between_end,
})
}
Snippet::All => Ok(SnippetResolution {
start: 0,
end: rope.len_chars(),
}),
}
}
}
#[cfg(test)]
#[path = "../../tests/snippet_resolution.rs"]
mod snippet_resolution;