1use arborium::HtmlFormat;
2use arborium::advanced::{Span, spans_to_html};
3use arborium::tree_sitter;
4use moire_types::LineRange;
5use moire_types::{ContextCodeLine, ContextSeparator, SourceContextLine};
6
7pub struct CutResult {
8 pub cut_source: String,
12 pub scope_range: LineRange,
16}
17
18const SCOPE_KINDS: &[&str] = &[
20 "function_item",
21 "closure_expression",
22 "impl_item",
23 "source_file",
24];
25
26fn body_kind_for_scope(scope_kind: &str) -> &'static str {
28 match scope_kind {
29 "function_item" | "closure_expression" => "block",
30 "impl_item" => "declaration_list",
31 _ => "source_file", }
33}
34
35fn display_scope_rows(
36 scope: tree_sitter::Node<'_>,
37 body: Option<tree_sitter::Node<'_>>,
38 body_children: &[tree_sitter::Node<'_>],
39) -> (usize, usize) {
40 if scope.kind() != "function_item" {
41 return (scope.start_position().row, scope.end_position().row);
42 }
43
44 if let (Some(first), Some(last)) = (body_children.first(), body_children.last()) {
46 return (first.start_position().row, last.end_position().row);
47 }
48
49 let mut row = body
51 .map(|body_node| body_node.start_position().row.saturating_add(1))
52 .unwrap_or_else(|| scope.start_position().row);
53 let scope_end_row = scope.end_position().row;
54 if row > scope_end_row {
55 row = scope_end_row;
56 }
57 (row, row)
58}
59
60const NEIGHBOR_COUNT: usize = 1;
63
64const COMPACT_NEIGHBOR_COUNT: usize = 0;
67
68pub fn cut_source(
77 content: &str,
78 lang_name: &str,
79 target_line: u32,
80 target_col: Option<u32>,
81) -> Option<CutResult> {
82 cut_source_with_neighbor_count(content, lang_name, target_line, target_col, NEIGHBOR_COUNT)
83}
84
85pub fn cut_source_compact(
87 content: &str,
88 lang_name: &str,
89 target_line: u32,
90 target_col: Option<u32>,
91) -> Option<CutResult> {
92 cut_source_with_neighbor_count(
93 content,
94 lang_name,
95 target_line,
96 target_col,
97 COMPACT_NEIGHBOR_COUNT,
98 )
99}
100
101fn cut_source_with_neighbor_count(
102 content: &str,
103 lang_name: &str,
104 target_line: u32,
105 target_col: Option<u32>,
106 neighbor_count: usize,
107) -> Option<CutResult> {
108 let ts_lang = arborium::get_language(lang_name)?;
109 let mut parser = tree_sitter::Parser::new();
110 parser.set_language(&ts_lang).ok()?;
111 let tree = parser.parse(content.as_bytes(), None)?;
112
113 let row = (target_line - 1) as usize;
114 let col = target_col.unwrap_or(0) as usize;
115 let point = tree_sitter::Point::new(row, col);
116
117 let node = tree
119 .root_node()
120 .named_descendant_for_point_range(point, point)?;
121
122 let scope = find_scope(node, point)?;
124
125 let source_lines: Vec<&str> = content.lines().collect();
126
127 let body_kind = body_kind_for_scope(scope.kind());
129 let is_source_file = scope.kind() == "source_file";
130
131 let (body_node, body_children): (Option<tree_sitter::Node>, Vec<tree_sitter::Node>) =
133 if is_source_file {
134 (
136 None,
137 (0..scope.named_child_count() as u32)
138 .filter_map(|i| scope.named_child(i))
139 .collect(),
140 )
141 } else {
142 let body = (0..scope.child_count() as u32)
144 .filter_map(|i| scope.child(i))
145 .find(|c| c.kind() == body_kind)?;
146 let children = (0..body.named_child_count() as u32)
147 .filter_map(|i| body.named_child(i))
148 .collect();
149 (Some(body), children)
150 };
151 let is_function_scope = scope.kind() == "function_item";
152 let (mut scope_start_row, mut scope_end_row) =
153 display_scope_rows(scope, body_node, &body_children);
154
155 if body_children.len() <= (neighbor_count * 2 + 1) && neighbor_count != COMPACT_NEIGHBOR_COUNT {
159 let scope_start = scope_start_row as u32 + 1;
160 let scope_end = scope_end_row as u32 + 1;
161 let cut = source_lines[(scope_start - 1) as usize..scope_end as usize].join("\n");
162 return Some(CutResult {
163 cut_source: cut,
164 scope_range: LineRange {
165 start: scope_start,
166 end: scope_end,
167 },
168 });
169 }
170
171 let target_idx = body_children
173 .iter()
174 .position(|child| contains_point(child, point))
175 .unwrap_or_else(|| {
176 body_children
178 .iter()
179 .enumerate()
180 .min_by_key(|(_, c)| {
181 let c_start = c.start_position().row;
182 let c_end = c.end_position().row;
183 if row >= c_start && row <= c_end {
184 0usize
185 } else if row < c_start {
186 c_start - row
187 } else {
188 row - c_end
189 }
190 })
191 .map(|(i, _)| i)
192 .unwrap_or(0)
193 });
194
195 let keep_start = target_idx.saturating_sub(neighbor_count);
197 let keep_end = (target_idx + neighbor_count + 1).min(body_children.len());
198
199 if is_function_scope && !body_children.is_empty() {
200 scope_start_row = body_children[keep_start].start_position().row;
201 scope_end_row = body_children[keep_end - 1].end_position().row;
202 }
203
204 let mut result_lines: Vec<String> = Vec::with_capacity(scope_end_row - scope_start_row + 1);
206
207 let mut cut_ranges: Vec<(usize, usize)> = Vec::new(); if !body_children.is_empty() {
216 if keep_start > 0 && !is_function_scope {
218 let cut_start = body_children[0].start_position().row;
219 let cut_end = if keep_start < body_children.len() {
220 body_children[keep_start]
221 .start_position()
222 .row
223 .saturating_sub(1)
224 } else {
225 body_children[body_children.len() - 1].end_position().row
226 };
227 if cut_end >= cut_start {
228 cut_ranges.push((cut_start, cut_end));
229 }
230 }
231
232 if keep_end < body_children.len() && !is_function_scope {
234 let cut_start = body_children[keep_end].start_position().row;
235 let cut_end = body_children[body_children.len() - 1].end_position().row;
236 if cut_end >= cut_start {
237 cut_ranges.push((cut_start, cut_end));
238 }
239 }
240
241 if neighbor_count == COMPACT_NEIGHBOR_COUNT {
244 for child in &body_children[keep_start..keep_end] {
245 collect_compact_block_elision_ranges(*child, &mut cut_ranges);
246 }
247 }
248 }
249
250 cut_ranges = merge_line_ranges(cut_ranges);
251
252 for row_idx in scope_start_row..=scope_end_row {
253 let in_cut = cut_ranges
254 .iter()
255 .find(|(s, e)| row_idx >= *s && row_idx <= *e);
256 if let Some(&(cut_start, _cut_end)) = in_cut {
257 if row_idx == cut_start {
258 let indent = if row_idx < source_lines.len() {
260 let line = source_lines[row_idx];
261 let trimmed = line.trim_start();
262 &line[..line.len() - trimmed.len()]
263 } else {
264 ""
265 };
266 result_lines.push(format!("{indent}/* ... */"));
267 } else {
268 result_lines.push(String::new());
270 }
271 } else if row_idx < source_lines.len() {
272 result_lines.push(source_lines[row_idx].to_string());
273 } else {
274 result_lines.push(String::new());
275 }
276 }
277
278 Some(CutResult {
279 cut_source: result_lines.join("\n"),
280 scope_range: LineRange {
281 start: scope_start_row as u32 + 1,
282 end: scope_end_row as u32 + 1,
283 },
284 })
285}
286
287fn collect_compact_block_elision_ranges(
288 node: tree_sitter::Node<'_>,
289 ranges: &mut Vec<(usize, usize)>,
290) {
291 if is_block_like_statement_node(node.kind()) {
292 let start_row = node.start_position().row;
293 let end_row = node.end_position().row;
294 if end_row > start_row + 1 {
295 ranges.push((start_row + 1, end_row - 1));
296 }
297 }
298
299 if node.kind() == "parameters" && node.parent().is_some_and(|p| p.kind() == "function_item") {
301 let start_row = node.start_position().row;
302 let end_row = node.end_position().row;
303 if end_row > start_row + MAX_PARAM_LIST_ROWS {
304 let first_param_end_row = (0..node.named_child_count() as u32)
305 .filter_map(|i| node.named_child(i))
306 .next()
307 .map(|p| p.end_position().row)
308 .unwrap_or(start_row);
309 let elide_start = first_param_end_row + 1;
310 let elide_end = end_row.saturating_sub(1);
311 if elide_end >= elide_start {
312 ranges.push((elide_start, elide_end));
313 }
314 }
315 }
316
317 for i in 0..node.child_count() as u32 {
318 if let Some(child) = node.child(i) {
319 collect_compact_block_elision_ranges(child, ranges);
320 }
321 }
322}
323
324fn find_scope<'a>(
331 node: tree_sitter::Node<'a>,
332 target: tree_sitter::Point,
333) -> Option<tree_sitter::Node<'a>> {
334 let mut current = node;
335 let mut found_scope: Option<tree_sitter::Node<'a>> = None;
336
337 loop {
338 if SCOPE_KINDS.contains(¤t.kind()) {
339 let child_count = count_body_children(¤t);
340 let on_closing_brace = target_is_on_closing_brace(¤t, target);
343 let is_terminal = matches!(
344 current.kind(),
345 "source_file" | "function_item" | "impl_item"
346 );
347 if child_count <= 1 && !is_terminal && !on_closing_brace {
348 found_scope = Some(current);
350 } else {
351 return Some(current);
352 }
353 }
354 match current.parent() {
355 Some(parent) => current = parent,
356 None => {
357 if current.kind() == "source_file" && contains_point(¤t, target) {
359 return Some(current);
360 }
361 return found_scope;
362 }
363 }
364 }
365}
366
367fn target_is_on_closing_brace(scope: &tree_sitter::Node, target: tree_sitter::Point) -> bool {
371 if scope.kind() == "source_file" {
372 return false;
373 }
374 let body_kind = body_kind_for_scope(scope.kind());
375 let body = (0..scope.child_count() as u32)
376 .filter_map(|i| scope.child(i))
377 .find(|c| c.kind() == body_kind);
378 match body {
379 Some(b) => target.row >= b.end_position().row,
380 None => false,
381 }
382}
383
384fn count_body_children(scope: &tree_sitter::Node) -> usize {
385 let body_kind = body_kind_for_scope(scope.kind());
386 if scope.kind() == "source_file" {
387 return scope.named_child_count();
388 }
389 let body = (0..scope.child_count() as u32)
390 .filter_map(|i| scope.child(i))
391 .find(|c| c.kind() == body_kind);
392 match body {
393 Some(b) => b.named_child_count(),
394 None => 0,
395 }
396}
397
398fn contains_point(node: &tree_sitter::Node, point: tree_sitter::Point) -> bool {
399 let start = node.start_position();
400 let end = node.end_position();
401 (point.row > start.row || (point.row == start.row && point.column >= start.column))
402 && (point.row < end.row || (point.row == end.row && point.column <= end.column))
403}
404
405const STATEMENT_KINDS: &[&str] = &[
407 "let_declaration",
408 "const_item",
409 "static_item",
410 "expression_statement",
411 "macro_invocation",
412 "function_item",
413 "if_expression",
414 "match_expression",
415 "for_expression",
416 "while_expression",
417 "loop_expression",
418 "return_expression",
419];
420
421const STATEMENT_BLOCK_INTERIOR_MAX_LINES: usize = 4;
423
424const MAX_PARAM_LIST_ROWS: usize = 3;
427
428pub fn extract_target_statement(
434 content: &str,
435 lang_name: &str,
436 target_line: u32,
437 target_col: Option<u32>,
438) -> Option<String> {
439 let ts_lang = arborium::get_language(lang_name)?;
440 let mut parser = tree_sitter::Parser::new();
441 parser.set_language(&ts_lang).ok()?;
442 let tree = parser.parse(content.as_bytes(), None)?;
443
444 let row = (target_line - 1) as usize;
445 let col = target_col.unwrap_or(0) as usize;
446 let point = tree_sitter::Point::new(row, col);
447
448 let node = tree
449 .root_node()
450 .named_descendant_for_point_range(point, point)?;
451
452 let mut current = node;
454 loop {
455 if STATEMENT_KINDS.contains(¤t.kind()) {
456 break;
457 }
458 if SCOPE_KINDS.contains(¤t.kind()) {
459 break;
460 }
461 match current.parent() {
462 Some(parent) => {
463 if SCOPE_KINDS.contains(&parent.kind()) {
464 let is_body_container =
470 current.kind() == "block" || current.kind() == "declaration_list";
471 if !is_body_container && STATEMENT_KINDS.contains(&parent.kind()) {
472 current = parent;
473 }
474 break;
475 }
476 current = parent;
477 }
478 None => break,
479 }
480 }
481
482 let statement = if current.kind() == "block" || current.kind() == "declaration_list" {
484 (0..current.named_child_count() as u32)
485 .filter_map(|i| current.named_child(i))
486 .find(|c| {
487 let s = c.start_position().row;
488 let e = c.end_position().row;
489 point.row >= s && point.row <= e
490 })
491 .unwrap_or(current)
492 } else {
493 current
494 };
495
496 let statement = {
499 let mut outer = statement;
500 while let Some(parent) = outer.parent() {
501 if SCOPE_KINDS.contains(&parent.kind()) {
502 break;
503 }
504 if STATEMENT_KINDS.contains(&parent.kind()) {
505 outer = parent;
506 continue;
507 }
508 break;
509 }
510 outer
511 };
512
513 let (text_start, text_start_row) = {
516 let mut start = statement.start_byte();
517 let mut row = statement.start_position().row;
518 for i in 0..statement.child_count() as u32 {
519 if let Some(child) = statement.child(i) {
520 if child.kind() == "attribute_item"
521 || child.kind() == "attribute"
522 || child.kind() == "attributes"
523 {
524 continue;
525 }
526 start = child.start_byte();
527 row = child.start_position().row;
528 break;
529 }
530 }
531 (start, row)
532 };
533
534 let text = &content[text_start..statement.end_byte()];
535 let snippet = compact_statement_text(statement, text, text_start_row);
536 if snippet.is_empty() {
537 return None;
538 }
539
540 Some(snippet)
541}
542
543pub fn extract_enclosing_fn(
555 content: &str,
556 lang_name: &str,
557 target_line: u32,
558 target_col: Option<u32>,
559) -> Option<String> {
560 if lang_name != "rust" {
561 return None;
562 }
563 let ts_lang = arborium::get_language(lang_name)?;
564 let mut parser = tree_sitter::Parser::new();
565 parser.set_language(&ts_lang).ok()?;
566 let tree = parser.parse(content.as_bytes(), None)?;
567
568 let row = (target_line - 1) as usize;
569 let col = target_col.unwrap_or(0) as usize;
570 let point = tree_sitter::Point::new(row, col);
571
572 let node = tree
573 .root_node()
574 .named_descendant_for_point_range(point, point)?;
575
576 let bytes = content.as_bytes();
577
578 let fn_node = {
582 let mut current = node;
583 loop {
584 if current.kind() == "function_item" {
585 break Some(current);
586 }
587 match current.parent() {
588 Some(parent) => current = parent,
589 None => break None,
590 }
591 }
592 }?;
593
594 let (modifiers, sig_without_modifiers) = extract_function_signature_text(content, &fn_node)?;
595 let mut qualifiers = collect_module_qualifiers(&fn_node, bytes);
596 if let Some(impl_type) = find_enclosing_impl_type_name(&fn_node, bytes) {
597 qualifiers.push(impl_type);
598 }
599
600 let qualified = if qualifiers.is_empty() {
601 sig_without_modifiers
602 } else {
603 format!("{}::{}", qualifiers.join("::"), sig_without_modifiers)
604 };
605
606 if modifiers.is_empty() {
607 Some(qualified)
608 } else {
609 Some(format!("{modifiers} {qualified}"))
610 }
611}
612
613fn collapse_ws_inline(text: &str) -> String {
614 text.split_whitespace().collect::<Vec<_>>().join(" ")
615}
616
617fn extract_function_signature_text(
621 content: &str,
622 fn_node: &tree_sitter::Node<'_>,
623) -> Option<(String, String)> {
624 let bytes = content.as_bytes();
625
626 let mut modifiers = String::new();
629 for i in 0..fn_node.child_count() as u32 {
630 let child = fn_node.child(i)?;
631 match child.kind() {
632 "attribute_item" | "visibility_modifier" => continue,
633 "function_modifiers" => {
634 modifiers = child.utf8_text(bytes).ok()?.to_string();
635 }
636 "fn" => break,
637 _ => {}
638 }
639 }
640
641 let name = fn_node.child_by_field_name("name")?.utf8_text(bytes).ok()?;
642
643 let params_node = fn_node.child_by_field_name("parameters")?;
644
645 let mut params_full: Vec<String> = Vec::new();
647 let mut params_slim: Vec<String> = Vec::new();
648 for i in 0..params_node.child_count() as u32 {
649 let child = params_node.child(i)?;
650 match child.kind() {
651 "parameter" => {
652 let full = collapse_ws_inline(child.utf8_text(bytes).ok()?);
653 params_full.push(full);
654 if let Some(pat) = child.child_by_field_name("pattern") {
655 params_slim.push(pat.utf8_text(bytes).ok()?.to_string());
656 }
657 }
658 "self_parameter" | "shorthand_self" => {
659 let self_text = collapse_ws_inline(child.utf8_text(bytes).ok()?);
660 params_full.push(self_text.clone());
661 params_slim.push(self_text);
662 }
663 _ => {}
664 }
665 }
666
667 let ret = fn_node
669 .child_by_field_name("return_type")
670 .and_then(|rt| rt.utf8_text(bytes).ok().map(collapse_ws_inline))
671 .map(|t| format!(" -> {t}"))
672 .unwrap_or_default();
673
674 const MAX_LEN: usize = 80;
678 let params_str = params_full.join(", ");
679 let candidate = format!("{name}({params_str}){ret}");
680 let sig = if candidate.len() <= MAX_LEN {
681 candidate
682 } else {
683 format!("{name}({}){ret}", params_slim.join(", "))
684 };
685
686 Some((modifiers, sig))
687}
688
689fn find_enclosing_impl_type_name(fn_node: &tree_sitter::Node<'_>, bytes: &[u8]) -> Option<String> {
690 let mut current = *fn_node;
691 while let Some(parent) = current.parent() {
692 if parent.kind() == "impl_item" {
693 let type_node = parent.child_by_field_name("type")?;
694 let raw = type_node.utf8_text(bytes).ok()?;
695 let collapsed = collapse_ws_inline(raw);
696 if collapsed.is_empty() {
697 return None;
698 }
699 return Some(collapsed);
700 }
701 current = parent;
702 }
703 None
704}
705
706fn collect_module_qualifiers(fn_node: &tree_sitter::Node<'_>, bytes: &[u8]) -> Vec<String> {
707 let mut rev_modules: Vec<String> = Vec::new();
708 let mut current = *fn_node;
709 while let Some(parent) = current.parent() {
710 if parent.kind() == "mod_item"
711 && let Some(name_node) = parent.child_by_field_name("name")
712 && let Ok(name) = name_node.utf8_text(bytes)
713 {
714 let collapsed = collapse_ws_inline(name);
715 if !collapsed.is_empty() {
716 rev_modules.push(collapsed);
717 }
718 }
719 current = parent;
720 }
721 rev_modules.reverse();
722 rev_modules
723}
724
725fn is_block_like_statement_node(kind: &str) -> bool {
726 kind == "block" || kind == "declaration_list" || kind == "match_block"
727}
728
729fn collect_statement_elision_ranges(node: tree_sitter::Node<'_>, ranges: &mut Vec<(usize, usize)>) {
730 if is_block_like_statement_node(node.kind()) {
731 let start_row = node.start_position().row;
732 let end_row = node.end_position().row;
733 if end_row > start_row + 1 {
734 let interior_start = start_row + 1;
735 let interior_end = end_row - 1;
736 let interior_len = interior_end - interior_start + 1;
737 let should_elide = node
738 .parent()
739 .is_some_and(|parent| parent.kind() == "function_item")
740 || interior_len > STATEMENT_BLOCK_INTERIOR_MAX_LINES;
741 if should_elide {
742 ranges.push((interior_start, interior_end));
743 }
744 }
745 }
746
747 if node.kind() == "parameters" && node.parent().is_some_and(|p| p.kind() == "function_item") {
749 let start_row = node.start_position().row;
750 let end_row = node.end_position().row;
751 if end_row > start_row + MAX_PARAM_LIST_ROWS {
752 let first_param_end_row = (0..node.named_child_count() as u32)
753 .filter_map(|i| node.named_child(i))
754 .next()
755 .map(|p| p.end_position().row)
756 .unwrap_or(start_row);
757 let elide_start = first_param_end_row + 1;
758 let elide_end = end_row.saturating_sub(1);
759 if elide_end >= elide_start {
760 ranges.push((elide_start, elide_end));
761 }
762 }
763 }
764
765 for i in 0..node.child_count() as u32 {
766 if let Some(child) = node.child(i) {
767 collect_statement_elision_ranges(child, ranges);
768 }
769 }
770}
771
772fn merge_line_ranges(mut ranges: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
773 if ranges.len() <= 1 {
774 return ranges;
775 }
776 ranges.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
777
778 let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
779 for (start, end) in ranges {
780 if let Some((_, last_end)) = merged.last_mut()
781 && start <= *last_end + 1
782 {
783 if end > *last_end {
784 *last_end = end;
785 }
786 continue;
787 }
788 merged.push((start, end));
789 }
790 merged
791}
792
793fn leading_ws_byte_len(line: &str) -> usize {
794 line.char_indices()
795 .find_map(|(idx, ch)| if ch.is_whitespace() { None } else { Some(idx) })
796 .unwrap_or(line.len())
797}
798
799fn normalize_statement_lines(lines: Vec<String>) -> String {
800 let Some(first_non_empty) = lines.iter().position(|line| !line.trim().is_empty()) else {
801 return String::new();
802 };
803 let Some(last_non_empty) = lines.iter().rposition(|line| !line.trim().is_empty()) else {
804 return String::new();
805 };
806
807 let slice = &lines[first_non_empty..=last_non_empty];
808 let continuation_indent = slice
809 .iter()
810 .enumerate()
811 .filter_map(|(idx, line)| {
812 if idx == 0 || line.trim().is_empty() {
813 return None;
814 }
815 Some(leading_ws_byte_len(line))
816 })
817 .min()
818 .unwrap_or(0);
819
820 let mut out = Vec::with_capacity(slice.len());
821 for (idx, line) in slice.iter().enumerate() {
822 let trimmed_end = line.trim_end_matches([' ', '\t']);
823 if trimmed_end.trim().is_empty() {
824 out.push(String::new());
825 continue;
826 }
827 let drop = if idx == 0 {
828 0
829 } else {
830 continuation_indent.min(leading_ws_byte_len(trimmed_end))
831 };
832 out.push(trimmed_end[drop..].to_string());
833 }
834 out.join("\n")
835}
836
837fn compact_statement_text(
838 statement: tree_sitter::Node<'_>,
839 text: &str,
840 text_start_row: usize,
841) -> String {
842 let mut lines: Vec<String> = text.lines().map(|line| line.to_string()).collect();
843 if lines.is_empty() {
844 return String::new();
845 }
846
847 let mut ranges = Vec::new();
848 collect_statement_elision_ranges(statement, &mut ranges);
849 let merged_ranges = merge_line_ranges(ranges);
850
851 for (start_row, end_row) in merged_ranges.into_iter().rev() {
852 if end_row < text_start_row {
853 continue;
854 }
855 let local_start = start_row.saturating_sub(text_start_row);
856 if local_start >= lines.len() {
857 continue;
858 }
859 let mut local_end = end_row.saturating_sub(text_start_row);
860 if local_end >= lines.len() {
861 local_end = lines.len() - 1;
862 }
863 if local_end < local_start {
864 continue;
865 }
866
867 let indent_len = leading_ws_byte_len(&lines[local_start]);
868 let indent = &lines[local_start][..indent_len];
869 lines.splice(local_start..=local_end, [format!("{indent}/* ... */")]);
870 }
871
872 normalize_statement_lines(lines)
873}
874
875fn collect_context_lines(
878 cut_result: &CutResult,
879 render: impl Fn(&str, usize, usize) -> String,
880) -> Vec<SourceContextLine> {
881 let source = &cut_result.cut_source;
882
883 let mut line_starts: Vec<usize> = vec![0];
884 for (i, &b) in source.as_bytes().iter().enumerate() {
885 if b == b'\n' {
886 line_starts.push(i + 1);
887 }
888 }
889
890 let mut result: Vec<SourceContextLine> = Vec::with_capacity(line_starts.len());
891 let mut skip_empty = false;
892
893 for (line_idx, &line_start) in line_starts.iter().enumerate() {
894 let line_end = line_starts
895 .get(line_idx + 1)
896 .map(|&s| s - 1)
897 .unwrap_or(source.len());
898 let line_text = &source[line_start..line_end];
899 let line_num = cut_result.scope_range.start + line_idx as u32;
900
901 if line_text.trim() == "/* ... */" {
902 result.push(SourceContextLine::Separator(ContextSeparator {
903 indent_cols: leading_indent_cols(line_text),
904 }));
905 skip_empty = true;
906 continue;
907 }
908
909 if skip_empty && line_text.trim().is_empty() {
910 continue;
911 }
912 skip_empty = false;
913
914 let content = render(source, line_start, line_end);
915 result.push(SourceContextLine::Line(ContextCodeLine {
916 line_num,
917 html: content,
918 }));
919 }
920
921 result
922}
923
924pub fn highlighted_context_lines(
926 cut_result: &CutResult,
927 lang_name: &str,
928) -> Vec<SourceContextLine> {
929 let source = &cut_result.cut_source;
930 let spans: Vec<Span> = arborium::Highlighter::new()
931 .highlight_spans(lang_name, source)
932 .unwrap_or_default();
933 let format = HtmlFormat::CustomElements;
934
935 collect_context_lines(cut_result, |src, line_start, line_end| {
936 let line_text = &src[line_start..line_end];
937 let line_spans: Vec<Span> = spans
938 .iter()
939 .filter(|s| (s.start as usize) < line_end && (s.end as usize) > line_start)
940 .map(|s| Span {
941 start: (s.start as usize).saturating_sub(line_start) as u32,
942 end: ((s.end as usize).min(line_end) - line_start) as u32,
943 capture: s.capture.clone(),
944 pattern_index: s.pattern_index,
945 })
946 .collect();
947 spans_to_html(line_text, line_spans, &format)
948 })
949}
950
951pub fn text_context_lines(cut_result: &CutResult) -> Vec<SourceContextLine> {
953 collect_context_lines(cut_result, |src, line_start, line_end| {
954 src[line_start..line_end].to_string()
955 })
956}
957
958fn leading_indent_cols(text: &str) -> u32 {
959 let mut cols = 0u32;
960 for ch in text.chars() {
961 match ch {
962 ' ' => cols += 1,
963 '\t' => cols += 4,
964 _ => break,
965 }
966 }
967 cols
968}
969
970#[cfg(test)]
971mod tests;