1use crate::client::SearchfoxClient;
2use crate::utils::searchfox_url_repo;
3use anyhow::Result;
4use scraper::{ElementRef, Html, Selector};
5
6#[derive(Debug, Clone)]
8pub struct NestingContext {
9 pub sym: String,
10 pub pretty_line: String,
11}
12
13impl SearchfoxClient {
14 pub async fn get_function_at_line(
19 &self,
20 path: &str,
21 line: usize,
22 ) -> Result<Vec<NestingContext>> {
23 let url = format!(
24 "{}/{}/source/{}",
25 self.base_url,
26 searchfox_url_repo(&self.repo),
27 path
28 );
29 let html = self.get_html(&url).await?;
30 Ok(parse_nesting_at_line(&html, line))
31 }
32}
33
34fn parse_nesting_at_line(html: &str, target_line: usize) -> Vec<NestingContext> {
35 let document = Html::parse_document(html);
36 let row_selector = Selector::parse(&format!("div[id=\"line-{}\"]", target_line)).unwrap();
37 let sticky_selector = Selector::parse("code.source-line").unwrap();
38
39 let row = match document.select(&row_selector).next() {
40 Some(r) => r,
41 None => return Vec::new(),
42 };
43
44 let mut contexts = Vec::new();
45 let mut node = row.parent();
46 while let Some(parent) = node {
47 if let Some(elem) = ElementRef::wrap(parent) {
48 let val = elem.value();
49 if val.has_class("nesting-container", scraper::CaseSensitivity::CaseSensitive) {
50 if let Some(sym) = val.attr("data-nesting-sym") {
51 let pretty_line = elem
52 .select(&sticky_selector)
53 .next()
54 .map(|code| code.text().collect::<String>().trim().to_string())
55 .unwrap_or_default();
56 contexts.push(NestingContext {
57 sym: sym.to_string(),
58 pretty_line,
59 });
60 }
61 }
62 }
63 node = parent.parent();
64 }
65
66 contexts
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 fn make_html(inner_line: &str) -> String {
74 format!(
75 r#"<html><body>
76<div class="nesting-container nesting-depth-1" data-nesting-sym="OuterClass">
77 <div role="row" id="line-1" class="source-line-with-number nesting-sticky-line">
78 <code role="cell" class="source-line">class OuterClass {{</code>
79 </div>
80 <div class="nesting-container nesting-depth-2" data-nesting-sym="OuterClass::innerMethod">
81 <div role="row" id="line-2" class="source-line-with-number nesting-sticky-line">
82 <code role="cell" class="source-line"> void innerMethod() {{</code>
83 </div>
84 <div role="row" id="line-3" class="source-line-with-number">
85 <code role="cell" class="source-line">{}</code>
86 </div>
87 </div>
88</div>
89<div role="row" id="line-4" class="source-line-with-number">
90 <code role="cell" class="source-line">// file scope</code>
91</div>
92</body></html>"#,
93 inner_line
94 )
95 }
96
97 #[test]
98 fn line_inside_inner_nesting() {
99 let html = make_html(" return 42;");
100 let result = parse_nesting_at_line(&html, 3);
101 assert_eq!(result.len(), 2);
102 assert_eq!(result[0].sym, "OuterClass::innerMethod");
103 assert_eq!(result[1].sym, "OuterClass");
104 }
105
106 #[test]
107 fn line_at_file_scope() {
108 let html = make_html(" return 42;");
109 let result = parse_nesting_at_line(&html, 4);
110 assert!(result.is_empty());
111 }
112
113 #[test]
114 fn line_not_found() {
115 let html = make_html(" return 42;");
116 let result = parse_nesting_at_line(&html, 99);
117 assert!(result.is_empty());
118 }
119
120 #[test]
121 fn sticky_line_text_captured() {
122 let html = make_html(" return 42;");
123 let result = parse_nesting_at_line(&html, 3);
124 assert!(!result[0].pretty_line.is_empty());
125 assert!(result[0].pretty_line.contains("innerMethod"));
126 }
127}