Skip to main content

pytest_language_server/providers/
references.rs

1//! Find-references provider for pytest fixtures.
2
3use super::Backend;
4use tower_lsp_server::jsonrpc::Result;
5use tower_lsp_server::ls_types::*;
6use tracing::{debug, info};
7
8impl Backend {
9    /// Handle references request
10    pub async fn handle_references(
11        &self,
12        params: ReferenceParams,
13    ) -> Result<Option<Vec<Location>>> {
14        let uri = params.text_document_position.text_document.uri;
15        let position = params.text_document_position.position;
16
17        info!(
18            "references request: uri={:?}, line={}, char={}",
19            uri, position.line, position.character
20        );
21
22        if let Some(file_path) = self.uri_to_path(&uri) {
23            info!(
24                "Looking for fixture references at {:?}:{}:{}",
25                file_path, position.line, position.character
26            );
27
28            // First, find which fixture we're looking at (definition or usage)
29            if let Some(fixture_name) = self.fixture_db.find_fixture_at_position(
30                &file_path,
31                position.line,
32                position.character,
33            ) {
34                info!(
35                    "Found fixture: {}, determining which definition to use",
36                    fixture_name
37                );
38
39                let current_line = Self::lsp_line_to_internal(position.line);
40                info!(
41                    "Current cursor position: line {} (1-indexed), char {}",
42                    current_line, position.character
43                );
44
45                // Determine which specific definition the user is referring to
46                // This could be a usage (resolve to definition) or clicking on a definition itself
47                let target_definition = self.fixture_db.find_fixture_definition(
48                    &file_path,
49                    position.line,
50                    position.character,
51                );
52
53                let (references, definition_to_include) = if let Some(definition) =
54                    target_definition
55                {
56                    info!(
57                        "Found definition via usage at {:?}:{}, finding references that resolve to it",
58                        definition.file_path, definition.line
59                    );
60                    // Find only references that resolve to this specific definition
61                    let refs = self.fixture_db.find_references_for_definition(&definition);
62                    (refs, Some(definition))
63                } else {
64                    // find_fixture_definition returns None if cursor is on a definition line (not a usage)
65                    // Check if we're on a fixture definition line
66                    let target_line = Self::lsp_line_to_internal(position.line);
67                    if let Some(definition_at_line) = self.fixture_db.get_definition_at_line(
68                        &file_path,
69                        target_line,
70                        &fixture_name,
71                    ) {
72                        info!(
73                            "Found definition at cursor position {:?}:{}, finding references that resolve to it",
74                            file_path, target_line
75                        );
76                        let refs = self
77                            .fixture_db
78                            .find_references_for_definition(&definition_at_line);
79                        (refs, Some(definition_at_line))
80                    } else {
81                        info!(
82                            "No specific definition found at cursor, finding all references by name"
83                        );
84                        // Fallback to finding all references by name (shouldn't normally happen)
85                        (self.fixture_db.find_fixture_references(&fixture_name), None)
86                    }
87                };
88
89                if references.is_empty() && definition_to_include.is_none() {
90                    info!("No references found for fixture: {}", fixture_name);
91                    return Ok(None);
92                }
93
94                info!(
95                    "Found {} references for fixture: {}",
96                    references.len(),
97                    fixture_name
98                );
99
100                // Log all references to help debug
101                for (i, r) in references.iter().enumerate() {
102                    debug!(
103                        "  Reference {}: {:?}:{} (chars {}-{})",
104                        i,
105                        r.file_path.file_name(),
106                        r.line,
107                        r.start_char,
108                        r.end_char
109                    );
110                }
111
112                // Check if current position is in the references
113                let has_current_position = references
114                    .iter()
115                    .any(|r| r.file_path == file_path && r.line == current_line);
116                info!(
117                    "Current position (line {}) in references: {}",
118                    current_line, has_current_position
119                );
120
121                // Convert references to LSP Locations
122                let mut locations = Vec::new();
123
124                // First, add the definition if we have one (LSP spec: includeDeclaration)
125                if let Some(ref def) = definition_to_include {
126                    let Some(def_uri) = self.path_to_uri(&def.file_path) else {
127                        return Ok(None);
128                    };
129
130                    let def_line = Self::internal_line_to_lsp(def.line);
131                    let def_location = Location {
132                        uri: def_uri,
133                        range: Self::create_point_range(def_line, 0),
134                    };
135                    locations.push(def_location);
136                }
137
138                // Then add all the usage references
139                // Skip references that are on the same line as the definition (to avoid duplicates)
140                let mut skipped_count = 0;
141                for reference in &references {
142                    // Check if this reference is the same location as the definition
143                    if let Some(ref def) = definition_to_include {
144                        if reference.file_path == def.file_path && reference.line == def.line {
145                            debug!(
146                                "Skipping reference at {:?}:{} (same as definition location)",
147                                reference.file_path, reference.line
148                            );
149                            skipped_count += 1;
150                            continue;
151                        }
152                    }
153
154                    let Some(ref_uri) = self.path_to_uri(&reference.file_path) else {
155                        continue;
156                    };
157
158                    let ref_line = Self::internal_line_to_lsp(reference.line);
159                    let location = Location {
160                        uri: ref_uri,
161                        range: Self::create_range(
162                            ref_line,
163                            reference.start_char as u32,
164                            ref_line,
165                            reference.end_char as u32,
166                        ),
167                    };
168                    debug!(
169                        "Adding reference location: {:?}:{} (chars {}-{})",
170                        reference.file_path.file_name(),
171                        reference.line,
172                        reference.start_char,
173                        reference.end_char
174                    );
175                    locations.push(location);
176                }
177
178                info!(
179                    "Returning {} locations (definition: {}, references: {}/{}, skipped: {})",
180                    locations.len(),
181                    if definition_to_include.is_some() {
182                        1
183                    } else {
184                        0
185                    },
186                    references.len() - skipped_count,
187                    references.len(),
188                    skipped_count
189                );
190                return Ok(Some(locations));
191            } else {
192                info!("No fixture found at this position");
193            }
194        }
195
196        Ok(None)
197    }
198}