1use crate::syntax::{SyntaxKind, SyntaxNode};
2
3fn is_block_element(kind: SyntaxKind) -> bool {
4 matches!(
5 kind,
6 SyntaxKind::PARAGRAPH
7 | SyntaxKind::FIGURE
8 | SyntaxKind::HEADING
9 | SyntaxKind::LIST
10 | SyntaxKind::DEFINITION_LIST
11 | SyntaxKind::BLOCK_QUOTE
12 | SyntaxKind::CODE_BLOCK
13 | SyntaxKind::SIMPLE_TABLE
14 | SyntaxKind::MULTILINE_TABLE
15 | SyntaxKind::PIPE_TABLE
16 | SyntaxKind::LINE_BLOCK
17 )
18}
19
20pub fn line_range_to_byte_offsets(
22 text: &str,
23 start_line: usize,
24 end_line: usize,
25) -> Option<(usize, usize)> {
26 if start_line == 0 || end_line == 0 || start_line > end_line {
27 return None;
28 }
29
30 let mut current_line = 1;
31 let mut start_offset = None;
32 let mut byte_offset = 0;
33
34 for line in text.split_inclusive('\n') {
35 if current_line == start_line {
36 start_offset = Some(byte_offset);
37 }
38
39 if current_line == end_line {
40 let end_offset = byte_offset + line.len();
42 return start_offset.map(|start| (start, end_offset));
43 }
44
45 byte_offset += line.len();
46 current_line += 1;
47 }
48
49 if current_line == end_line + 1 && start_offset.is_some() {
51 return start_offset.map(|start| (start, byte_offset));
53 }
54
55 None
57}
58
59fn find_enclosing_block(node: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
61 let text_offset = rowan::TextSize::try_from(offset).ok()?;
62
63 let token = node.token_at_offset(text_offset).right_biased()?;
65 let mut current = token.parent()?;
66
67 loop {
69 if is_block_element(current.kind()) {
70 return Some(current);
71 }
72
73 current = current.parent()?;
74 }
75}
76
77fn find_expandable_container(node: &SyntaxNode) -> Option<SyntaxNode> {
79 let mut current = node.clone();
80 let mut best: Option<SyntaxNode> = None;
81 let mut best_priority = 0u8;
82
83 loop {
84 let priority = match current.kind() {
85 SyntaxKind::LIST => 1,
86 SyntaxKind::DEFINITION_LIST => 2,
87 SyntaxKind::DEFINITION_ITEM => 3,
88 SyntaxKind::LINE_BLOCK => 2,
89 SyntaxKind::BLOCK_QUOTE | SyntaxKind::FENCED_DIV => 4,
90 _ => 0,
91 };
92 if priority >= best_priority && priority > 0 {
93 best_priority = priority;
94 best = Some(current.clone());
95 }
96
97 let Some(parent) = current.parent() else {
98 break;
99 };
100 current = parent;
101 }
102
103 best
104}
105
106pub fn expand_byte_range_to_blocks(tree: &SyntaxNode, start: usize, end: usize) -> (usize, usize) {
119 if start >= end {
121 if let Some(block) = find_enclosing_block(tree, start) {
123 let range = block.text_range();
124 return (range.start().into(), range.end().into());
125 }
126 return (start, start);
127 }
128
129 let start_block = find_enclosing_block(tree, start);
131 let end_block = find_enclosing_block(tree, end.saturating_sub(1)); let (mut expanded_start, mut expanded_end) = match (start_block, end_block) {
134 (Some(start_node), Some(end_node)) => {
135 let start_range = start_node.text_range();
136 let end_range = end_node.text_range();
137 (start_range.start().into(), end_range.end().into())
138 }
139 (Some(start_node), None) => {
140 let range = start_node.text_range();
142 (range.start().into(), end)
143 }
144 (None, Some(end_node)) => {
145 let range = end_node.text_range();
147 (start, range.end().into())
148 }
149 (None, None) => {
150 return (start, end);
152 }
153 };
154
155 if let Some(start_node) = find_enclosing_block(tree, expanded_start)
158 && let Some(container) = find_expandable_container(&start_node)
159 {
160 let container_range = container.text_range();
161 expanded_start = expanded_start.min(container_range.start().into());
162 expanded_end = expanded_end.max(container_range.end().into());
163 }
164
165 if let Some(end_node) = find_enclosing_block(tree, expanded_end.saturating_sub(1))
166 && let Some(container) = find_expandable_container(&end_node)
167 {
168 let container_range = container.text_range();
169 expanded_start = expanded_start.min(container_range.start().into());
170 expanded_end = expanded_end.max(container_range.end().into());
171 }
172
173 (expanded_start, expanded_end)
174}
175
176pub fn find_incremental_restart_offset(tree: &SyntaxNode, start: usize, end: usize) -> usize {
181 let (expanded_start, _) = expand_byte_range_to_blocks(tree, start, end);
182 let Some(block) = find_enclosing_block(tree, expanded_start) else {
183 return expanded_start;
184 };
185
186 if let Some(prev) = block.prev_sibling()
187 && is_block_element(prev.kind())
188 {
189 return prev.text_range().start().into();
190 }
191
192 expanded_start
193}
194
195pub fn expand_line_range_to_blocks(
209 tree: &SyntaxNode,
210 text: &str,
211 start_line: usize,
212 end_line: usize,
213) -> Option<(usize, usize)> {
214 let (start, end) = line_range_to_byte_offsets(text, start_line, end_line)?;
215 Some(expand_byte_range_to_blocks(tree, start, end))
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::options::ParserOptions;
222
223 fn parse_test_doc(input: &str) -> SyntaxNode {
224 crate::parse(input, Some(ParserOptions::default()))
225 }
226
227 #[test]
228 fn test_line_range_to_byte_offsets() {
229 let doc = "Line 1\nLine 2\nLine 3\n";
230
231 let (start, end) = line_range_to_byte_offsets(doc, 1, 1).unwrap();
233 assert_eq!(&doc[start..end], "Line 1\n");
234
235 let (start, end) = line_range_to_byte_offsets(doc, 2, 2).unwrap();
237 assert_eq!(&doc[start..end], "Line 2\n");
238
239 let (start, end) = line_range_to_byte_offsets(doc, 1, 2).unwrap();
241 assert_eq!(&doc[start..end], "Line 1\nLine 2\n");
242
243 assert!(line_range_to_byte_offsets(doc, 0, 1).is_none()); assert!(line_range_to_byte_offsets(doc, 2, 1).is_none()); assert!(line_range_to_byte_offsets(doc, 1, 10).is_none()); }
248
249 #[test]
250 fn test_expand_single_paragraph() {
251 let doc = "Para 1\n\nPara 2\n\nPara 3\n";
252 let tree = parse_test_doc(doc);
253
254 let (start, end) = expand_line_range_to_blocks(&tree, doc, 3, 3).unwrap();
256
257 let selected = &doc[start..end];
258 assert!(selected.contains("Para 2"), "Range should include Para 2");
259 assert!(
260 !selected.contains("Para 1"),
261 "Range should not include Para 1"
262 );
263 assert!(
264 !selected.contains("Para 3"),
265 "Range should not include Para 3"
266 );
267 }
268
269 #[test]
270 fn test_expand_code_block() {
271 let doc = "Text before\n\n```rust\nfn main() {}\n```\n\nText after\n";
272 let tree = parse_test_doc(doc);
273
274 let (start, end) = expand_line_range_to_blocks(&tree, doc, 4, 4).unwrap();
277
278 let selected = &doc[start..end];
280 assert!(
281 selected.contains("```rust"),
282 "Range should include opening fence"
283 );
284 assert!(
285 selected.contains("fn main() {}"),
286 "Range should include code"
287 );
288 assert!(
289 selected.contains("```"),
290 "Range should include closing fence"
291 );
292 assert!(
293 !selected.contains("Text before"),
294 "Range should not include text before"
295 );
296 assert!(
297 !selected.contains("Text after"),
298 "Range should not include text after"
299 );
300 }
301
302 #[test]
303 fn test_expand_list_item_to_full_list() {
304 let doc = "Before\n\n- Item 1\n- Item 2\n- Item 3\n\nAfter\n";
305 let tree = parse_test_doc(doc);
306
307 let (start, end) = expand_line_range_to_blocks(&tree, doc, 4, 4).unwrap();
309
310 let selected = &doc[start..end];
312 assert!(selected.contains("Item 1"), "Range should include Item 1");
313 assert!(selected.contains("Item 2"), "Range should include Item 2");
314 assert!(selected.contains("Item 3"), "Range should include Item 3");
315 assert!(
316 !selected.contains("Before"),
317 "Range should not include Before"
318 );
319 assert!(
320 !selected.contains("After"),
321 "Range should not include After"
322 );
323 }
324
325 #[test]
326 fn test_single_line_expands_to_block() {
327 let doc = "# Heading\n\nParagraph text here.\n";
328 let tree = parse_test_doc(doc);
329
330 let (start, end) = expand_line_range_to_blocks(&tree, doc, 3, 3).unwrap();
332
333 let selected = &doc[start..end];
335 assert!(
336 selected.contains("Paragraph text here."),
337 "Range should include paragraph"
338 );
339 assert!(
340 !selected.contains("Heading"),
341 "Range should not include heading"
342 );
343 }
344
345 #[test]
346 fn test_expand_blockquote() {
347 let doc = "Before\n\n> Line 1\n> Line 2\n> Line 3\n\nAfter\n";
348 let tree = parse_test_doc(doc);
349
350 let result = expand_line_range_to_blocks(&tree, doc, 4, 4);
352 assert!(result.is_some(), "Failed to expand range for line 4");
353 let (start, end) = result.unwrap();
354
355 let selected = &doc[start..end];
358
359 assert!(selected.contains("Line 1"), "Range should include Line 1");
361 assert!(selected.contains("Line 2"), "Range should include Line 2");
362 assert!(selected.contains("Line 3"), "Range should include Line 3");
363 assert!(
364 !selected.contains("Before"),
365 "Range should not include Before"
366 );
367 assert!(
368 !selected.contains("After"),
369 "Range should not include After"
370 );
371
372 }
374}