1use collections::HashMap;
  2use language::BufferSnapshot;
  3use std::ops::Range;
  4use util::RangeExt;
  5
  6use crate::{
  7    declaration::Identifier,
  8    excerpt::{EditPredictionExcerpt, EditPredictionExcerptText},
  9};
 10
 11#[derive(Debug, Clone)]
 12pub struct Reference {
 13    pub identifier: Identifier,
 14    pub range: Range<usize>,
 15    pub region: ReferenceRegion,
 16}
 17
 18#[derive(Copy, Clone, Debug, Eq, PartialEq)]
 19pub enum ReferenceRegion {
 20    Breadcrumb,
 21    Nearby,
 22}
 23
 24pub fn references_in_excerpt(
 25    excerpt: &EditPredictionExcerpt,
 26    excerpt_text: &EditPredictionExcerptText,
 27    snapshot: &BufferSnapshot,
 28) -> HashMap<Identifier, Vec<Reference>> {
 29    let mut references = references_in_range(
 30        excerpt.range.clone(),
 31        excerpt_text.body.as_str(),
 32        ReferenceRegion::Nearby,
 33        snapshot,
 34    );
 35
 36    for ((_, range), text) in excerpt
 37        .parent_declarations
 38        .iter()
 39        .zip(excerpt_text.parent_signatures.iter())
 40    {
 41        references.extend(references_in_range(
 42            range.clone(),
 43            text.as_str(),
 44            ReferenceRegion::Breadcrumb,
 45            snapshot,
 46        ));
 47    }
 48
 49    let mut identifier_to_references: HashMap<Identifier, Vec<Reference>> = HashMap::default();
 50    for reference in references {
 51        identifier_to_references
 52            .entry(reference.identifier.clone())
 53            .or_insert_with(Vec::new)
 54            .push(reference);
 55    }
 56    identifier_to_references
 57}
 58
 59/// Finds all nodes which have a "variable" match from the highlights query within the offset range.
 60pub fn references_in_range(
 61    range: Range<usize>,
 62    range_text: &str,
 63    reference_region: ReferenceRegion,
 64    buffer: &BufferSnapshot,
 65) -> Vec<Reference> {
 66    let mut matches = buffer
 67        .syntax
 68        .matches(range.clone(), &buffer.text, |grammar| {
 69            grammar
 70                .highlights_config
 71                .as_ref()
 72                .map(|config| &config.query)
 73        });
 74
 75    let mut references = Vec::new();
 76    let mut last_added_range = None;
 77    while let Some(mat) = matches.peek() {
 78        let config = matches.grammars()[mat.grammar_index]
 79            .highlights_config
 80            .as_ref();
 81
 82        if let Some(config) = config {
 83            for capture in mat.captures {
 84                if config.identifier_capture_indices.contains(&capture.index) {
 85                    let node_range = capture.node.byte_range();
 86
 87                    // sometimes multiple highlight queries match - this deduplicates them
 88                    if Some(node_range.clone()) == last_added_range {
 89                        continue;
 90                    }
 91
 92                    if !range.contains_inclusive(&node_range) {
 93                        continue;
 94                    }
 95
 96                    let identifier_text =
 97                        &range_text[node_range.start - range.start..node_range.end - range.start];
 98
 99                    references.push(Reference {
100                        identifier: Identifier {
101                            name: identifier_text.into(),
102                            language_id: mat.language.id(),
103                        },
104                        range: node_range.clone(),
105                        region: reference_region,
106                    });
107                    last_added_range = Some(node_range);
108                }
109            }
110        }
111
112        matches.advance();
113    }
114    references
115}
116
117#[cfg(test)]
118mod test {
119    use gpui::{TestAppContext, prelude::*};
120    use indoc::indoc;
121    use language::{BufferSnapshot, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
122
123    use crate::reference::{ReferenceRegion, references_in_range};
124
125    #[gpui::test]
126    fn test_identifier_node_truncated(cx: &mut TestAppContext) {
127        let code = indoc! { r#"
128            fn main() {
129                add(1, 2);
130            }
131
132            fn add(a: i32, b: i32) -> i32 {
133                a + b
134            }
135        "# };
136        let buffer = create_buffer(code, cx);
137
138        let range = 0..35;
139        let references = references_in_range(
140            range.clone(),
141            &code[range],
142            ReferenceRegion::Breadcrumb,
143            &buffer,
144        );
145        assert_eq!(references.len(), 2);
146        assert_eq!(references[0].identifier.name.as_ref(), "main");
147        assert_eq!(references[1].identifier.name.as_ref(), "add");
148    }
149
150    fn create_buffer(text: &str, cx: &mut TestAppContext) -> BufferSnapshot {
151        let buffer =
152            cx.new(|cx| language::Buffer::local(text, cx).with_language(rust_lang().into(), cx));
153        buffer.read_with(cx, |buffer, _| buffer.snapshot())
154    }
155
156    fn rust_lang() -> Language {
157        Language::new(
158            LanguageConfig {
159                name: "Rust".into(),
160                matcher: LanguageMatcher {
161                    path_suffixes: vec!["rs".to_string()],
162                    ..Default::default()
163                },
164                ..Default::default()
165            },
166            Some(tree_sitter_rust::LANGUAGE.into()),
167        )
168        .with_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
169        .unwrap()
170        .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
171        .unwrap()
172    }
173}