pytest_language_server/providers/
call_hierarchy.rs1use super::Backend;
8use tower_lsp_server::jsonrpc::Result;
9use tower_lsp_server::ls_types::*;
10use tracing::info;
11
12impl Backend {
13 pub async fn handle_prepare_call_hierarchy(
17 &self,
18 params: CallHierarchyPrepareParams,
19 ) -> Result<Option<Vec<CallHierarchyItem>>> {
20 let uri = params.text_document_position_params.text_document.uri;
21 let position = params.text_document_position_params.position;
22
23 info!(
24 "prepareCallHierarchy request: uri={:?}, line={}, char={}",
25 uri, position.line, position.character
26 );
27
28 if let Some(file_path) = self.uri_to_path(&uri) {
29 if let Some(definition) = self.fixture_db.find_fixture_or_definition_at_position(
31 &file_path,
32 position.line,
33 position.character,
34 ) {
35 let Some(def_uri) = self.path_to_uri(&definition.file_path) else {
36 return Ok(None);
37 };
38
39 let def_line = Self::internal_line_to_lsp(definition.line);
40 let selection_range = Range {
41 start: Position {
42 line: def_line,
43 character: definition.start_char as u32,
44 },
45 end: Position {
46 line: def_line,
47 character: definition.end_char as u32,
48 },
49 };
50
51 let range = Self::create_point_range(def_line, 0);
53
54 let item = CallHierarchyItem {
55 name: definition.name.clone(),
56 kind: SymbolKind::FUNCTION,
57 tags: None,
58 detail: Some(format!(
59 "@pytest.fixture{}",
60 if definition.scope != crate::fixtures::types::FixtureScope::Function {
61 format!("(scope=\"{}\")", definition.scope.as_str())
62 } else {
63 String::new()
64 }
65 )),
66 uri: def_uri,
67 range,
68 selection_range,
69 data: None,
70 };
71
72 info!("Returning call hierarchy item: {:?}", item);
73 return Ok(Some(vec![item]));
74 }
75 }
76
77 Ok(None)
78 }
79
80 pub async fn handle_incoming_calls(
84 &self,
85 params: CallHierarchyIncomingCallsParams,
86 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
87 let item = ¶ms.item;
88 info!("incomingCalls request for: {}", item.name);
89
90 let Some(file_path) = self.uri_to_path(&item.uri) else {
91 return Ok(None);
92 };
93
94 let Some(defs) = self.fixture_db.definitions.get(&item.name) else {
96 return Ok(None);
97 };
98
99 let Some(definition) = defs.iter().find(|d| d.file_path == file_path) else {
101 return Ok(None);
102 };
103
104 let references = self.fixture_db.find_references_for_definition(definition);
106
107 let mut incoming_calls: Vec<CallHierarchyIncomingCall> = Vec::new();
108
109 for usage in references {
110 if usage.file_path == definition.file_path && usage.line == definition.line {
112 continue;
113 }
114
115 let Some(usage_uri) = self.path_to_uri(&usage.file_path) else {
116 continue;
117 };
118
119 let usage_line = Self::internal_line_to_lsp(usage.line);
120 let from_range = Range {
121 start: Position {
122 line: usage_line,
123 character: usage.start_char as u32,
124 },
125 end: Position {
126 line: usage_line,
127 character: usage.end_char as u32,
128 },
129 };
130
131 let caller_name = self
133 .fixture_db
134 .find_containing_function(&usage.file_path, usage.line)
135 .unwrap_or_else(|| "<unknown>".to_string());
136
137 let from_item = CallHierarchyItem {
138 name: caller_name,
139 kind: SymbolKind::FUNCTION,
140 tags: None,
141 detail: Some(usage.file_path.display().to_string()),
142 uri: usage_uri,
143 range: from_range,
144 selection_range: from_range,
145 data: None,
146 };
147
148 incoming_calls.push(CallHierarchyIncomingCall {
149 from: from_item,
150 from_ranges: vec![from_range],
151 });
152 }
153
154 info!("Found {} incoming calls", incoming_calls.len());
155 Ok(Some(incoming_calls))
156 }
157
158 pub async fn handle_outgoing_calls(
162 &self,
163 params: CallHierarchyOutgoingCallsParams,
164 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
165 let item = ¶ms.item;
166 info!("outgoingCalls request for: {}", item.name);
167
168 let Some(file_path) = self.uri_to_path(&item.uri) else {
169 return Ok(None);
170 };
171
172 let Some(defs) = self.fixture_db.definitions.get(&item.name) else {
174 return Ok(None);
175 };
176
177 let Some(definition) = defs.iter().find(|d| d.file_path == file_path) else {
179 return Ok(None);
180 };
181
182 let mut outgoing_calls: Vec<CallHierarchyOutgoingCall> = Vec::new();
183
184 for dep_name in &definition.dependencies {
186 if let Some(dep_def) = self
188 .fixture_db
189 .resolve_fixture_for_file(&file_path, dep_name)
190 {
191 let Some(dep_uri) = self.path_to_uri(&dep_def.file_path) else {
192 continue;
193 };
194
195 let dep_line = Self::internal_line_to_lsp(dep_def.line);
196 let to_range = Range {
197 start: Position {
198 line: dep_line,
199 character: dep_def.start_char as u32,
200 },
201 end: Position {
202 line: dep_line,
203 character: dep_def.end_char as u32,
204 },
205 };
206
207 let to_item = CallHierarchyItem {
208 name: dep_def.name.clone(),
209 kind: SymbolKind::FUNCTION,
210 tags: None,
211 detail: Some(format!(
212 "@pytest.fixture{}",
213 if dep_def.scope != crate::fixtures::types::FixtureScope::Function {
214 format!("(scope=\"{}\")", dep_def.scope.as_str())
215 } else {
216 String::new()
217 }
218 )),
219 uri: dep_uri,
220 range: Self::create_point_range(dep_line, 0),
221 selection_range: to_range,
222 data: None,
223 };
224
225 let from_ranges = self
228 .find_parameter_ranges(&file_path, definition.line, dep_name)
229 .unwrap_or_else(|| vec![to_range]);
230
231 outgoing_calls.push(CallHierarchyOutgoingCall {
232 to: to_item,
233 from_ranges,
234 });
235 }
236 }
237
238 info!("Found {} outgoing calls", outgoing_calls.len());
239 Ok(Some(outgoing_calls))
240 }
241
242 fn find_parameter_ranges(
244 &self,
245 file_path: &std::path::Path,
246 line: usize,
247 param_name: &str,
248 ) -> Option<Vec<Range>> {
249 let content = self.fixture_db.file_cache.get(file_path)?;
250 let lines: Vec<&str> = content.lines().collect();
251
252 let line_content = lines.get(line.saturating_sub(1))?;
254
255 if let Some(start) = line_content.find(param_name) {
257 let lsp_line = Self::internal_line_to_lsp(line);
258 let range = Range {
259 start: Position {
260 line: lsp_line,
261 character: start as u32,
262 },
263 end: Position {
264 line: lsp_line,
265 character: (start + param_name.len()) as u32,
266 },
267 };
268 return Some(vec![range]);
269 }
270
271 None
272 }
273}