Skip to main content

searchfox_lib/
nesting.rs

1use crate::client::SearchfoxClient;
2use crate::utils::searchfox_url_repo;
3use anyhow::Result;
4use scraper::{ElementRef, Html, Selector};
5
6/// One level of nesting context: the symbol name and the source text of the opening line.
7#[derive(Debug, Clone)]
8pub struct NestingContext {
9    pub sym: String,
10    pub pretty_line: String,
11}
12
13impl SearchfoxClient {
14    /// Return the nesting context (function/class/namespace chain) that contains `line` in `path`.
15    ///
16    /// The returned Vec is ordered from innermost to outermost. Returns an empty Vec when the
17    /// line is at file scope (not inside any nesting block).
18    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}