deps_lsp/handlers/
inlay_hints.rs

1//! Inlay hints handler using ecosystem trait delegation.
2//!
3//! This handler uses the ecosystem registry to delegate inlay hint generation
4//! to the appropriate ecosystem implementation.
5
6use crate::config::{DepsConfig, InlayHintsConfig};
7use crate::document::{ServerState, ensure_document_loaded};
8use deps_core::EcosystemConfig;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tower_lsp_server::Client;
12use tower_lsp_server::ls_types::{InlayHint, InlayHintParams};
13
14/// Handles inlay hint requests using trait-based delegation.
15///
16/// Returns version status hints for all registry dependencies in the document.
17/// Gracefully degrades by returning empty vec on any errors.
18pub async fn handle_inlay_hints(
19    state: Arc<ServerState>,
20    params: InlayHintParams,
21    config: &InlayHintsConfig,
22    client: Client,
23    full_config: Arc<RwLock<DepsConfig>>,
24) -> Vec<InlayHint> {
25    if !config.enabled {
26        return vec![];
27    }
28
29    let uri = &params.text_document.uri;
30
31    // Ensure document is loaded (cold start support)
32    if !ensure_document_loaded(uri, Arc::clone(&state), client, Arc::clone(&full_config)).await {
33        tracing::warn!("Could not load document for inlay hints: {:?}", uri);
34        return vec![];
35    }
36
37    // Single document lookup: extract all needed data at once
38    let doc = match state.get_document(uri) {
39        Some(d) => d,
40        None => {
41            tracing::warn!("Document not found: {:?}", uri);
42            return vec![];
43        }
44    };
45
46    let ecosystem = match state.ecosystem_registry.get(doc.ecosystem_id) {
47        Some(e) => e,
48        None => {
49            tracing::warn!("Ecosystem not found: {}", doc.ecosystem_id);
50            return vec![];
51        }
52    };
53
54    let parse_result = match doc.parse_result() {
55        Some(p) => p,
56        None => return vec![],
57    };
58
59    // Get loading indicator config
60    let loading_config = { full_config.read().await.loading_indicator.clone() };
61
62    let ecosystem_config = EcosystemConfig {
63        show_up_to_date_hints: true,
64        up_to_date_text: config.up_to_date_text.clone(),
65        needs_update_text: config.needs_update_text.clone(),
66        loading_text: loading_config.loading_text,
67        show_loading_hints: loading_config.enabled && loading_config.fallback_to_hints,
68    };
69
70    // Generate hints while holding the lock
71    ecosystem
72        .generate_inlay_hints(
73            parse_result,
74            &doc.cached_versions,
75            &doc.resolved_versions,
76            doc.loading_state,
77            &ecosystem_config,
78        )
79        .await
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::document::ServerState;
86    use crate::test_utils::test_helpers::create_test_client_and_config;
87    use tower_lsp_server::ls_types::{TextDocumentIdentifier, Uri};
88
89    // Generic tests (no feature flag required)
90
91    #[test]
92    fn test_handle_inlay_hints_disabled() {
93        let config = InlayHintsConfig {
94            enabled: false,
95            up_to_date_text: "✅".to_string(),
96            needs_update_text: "❌ {}".to_string(),
97        };
98
99        assert!(!config.enabled);
100    }
101
102    #[tokio::test]
103    async fn test_handle_inlay_hints_disabled_returns_empty() {
104        let state = Arc::new(ServerState::new());
105        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
106        let config = InlayHintsConfig {
107            enabled: false,
108            up_to_date_text: "✅".to_string(),
109            needs_update_text: "❌ {}".to_string(),
110        };
111
112        let params = InlayHintParams {
113            text_document: TextDocumentIdentifier { uri },
114            work_done_progress_params: Default::default(),
115            range: tower_lsp_server::ls_types::Range::new(
116                tower_lsp_server::ls_types::Position::new(0, 0),
117                tower_lsp_server::ls_types::Position::new(100, 0),
118            ),
119        };
120
121        let (client, full_config) = create_test_client_and_config();
122        let result = handle_inlay_hints(state, params, &config, client, full_config).await;
123        assert!(result.is_empty());
124    }
125
126    #[tokio::test]
127    async fn test_handle_inlay_hints_missing_document() {
128        let state = Arc::new(ServerState::new());
129        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
130        let config = InlayHintsConfig {
131            enabled: true,
132            up_to_date_text: "✅".to_string(),
133            needs_update_text: "❌ {}".to_string(),
134        };
135
136        let params = InlayHintParams {
137            text_document: TextDocumentIdentifier { uri },
138            work_done_progress_params: Default::default(),
139            range: tower_lsp_server::ls_types::Range::new(
140                tower_lsp_server::ls_types::Position::new(0, 0),
141                tower_lsp_server::ls_types::Position::new(100, 0),
142            ),
143        };
144
145        let (client, full_config) = create_test_client_and_config();
146        let result = handle_inlay_hints(state, params, &config, client, full_config).await;
147        assert!(result.is_empty());
148    }
149
150    // Cargo-specific tests
151    #[cfg(feature = "cargo")]
152    mod cargo_tests {
153        use super::*;
154        use crate::document::{DocumentState, Ecosystem};
155
156        #[tokio::test]
157        async fn test_handle_inlay_hints() {
158            let state = Arc::new(ServerState::new());
159            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
160            let config = InlayHintsConfig {
161                enabled: true,
162                up_to_date_text: "✅".to_string(),
163                needs_update_text: "❌ {}".to_string(),
164            };
165
166            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
167            let content = r#"[dependencies]
168serde = "1.0.0"
169"#
170            .to_string();
171
172            let parse_result = ecosystem
173                .parse_manifest(&content, &uri)
174                .await
175                .expect("Failed to parse manifest");
176
177            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
178            state.update_document(uri.clone(), doc_state);
179
180            let params = InlayHintParams {
181                text_document: TextDocumentIdentifier { uri },
182                work_done_progress_params: Default::default(),
183                range: tower_lsp_server::ls_types::Range::new(
184                    tower_lsp_server::ls_types::Position::new(0, 0),
185                    tower_lsp_server::ls_types::Position::new(100, 0),
186                ),
187            };
188
189            let (client, full_config) = create_test_client_and_config();
190            let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
191            // Test passes if no panic occurs
192        }
193
194        #[tokio::test]
195        async fn test_handle_inlay_hints_no_parse_result() {
196            let state = Arc::new(ServerState::new());
197            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
198            let config = InlayHintsConfig {
199                enabled: true,
200                up_to_date_text: "✅".to_string(),
201                needs_update_text: "❌ {}".to_string(),
202            };
203
204            let doc_state = DocumentState::new(Ecosystem::Cargo, String::new(), vec![]);
205            state.update_document(uri.clone(), doc_state);
206
207            let params = InlayHintParams {
208                text_document: TextDocumentIdentifier { uri },
209                work_done_progress_params: Default::default(),
210                range: tower_lsp_server::ls_types::Range::new(
211                    tower_lsp_server::ls_types::Position::new(0, 0),
212                    tower_lsp_server::ls_types::Position::new(100, 0),
213                ),
214            };
215
216            let (client, full_config) = create_test_client_and_config();
217            let result = handle_inlay_hints(state, params, &config, client, full_config).await;
218            assert!(result.is_empty());
219        }
220
221        #[tokio::test]
222        async fn test_handle_inlay_hints_custom_config() {
223            let state = Arc::new(ServerState::new());
224            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
225            let config = InlayHintsConfig {
226                enabled: true,
227                up_to_date_text: "OK".to_string(),
228                needs_update_text: "UPDATE: {}".to_string(),
229            };
230
231            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
232            let content = r#"[dependencies]
233serde = "1.0.0"
234"#
235            .to_string();
236
237            let parse_result = ecosystem
238                .parse_manifest(&content, &uri)
239                .await
240                .expect("Failed to parse manifest");
241
242            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
243            state.update_document(uri.clone(), doc_state);
244
245            let params = InlayHintParams {
246                text_document: TextDocumentIdentifier { uri },
247                work_done_progress_params: Default::default(),
248                range: tower_lsp_server::ls_types::Range::new(
249                    tower_lsp_server::ls_types::Position::new(0, 0),
250                    tower_lsp_server::ls_types::Position::new(100, 0),
251                ),
252            };
253
254            let (client, full_config) = create_test_client_and_config();
255            let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
256            // Test passes if no panic occurs
257        }
258    }
259
260    // npm-specific tests
261    #[cfg(feature = "npm")]
262    mod npm_tests {
263        use super::*;
264        use crate::document::DocumentState;
265
266        #[tokio::test]
267        async fn test_handle_inlay_hints() {
268            let state = Arc::new(ServerState::new());
269            let uri = Uri::from_file_path("/test/package.json").unwrap();
270            let config = InlayHintsConfig {
271                enabled: true,
272                up_to_date_text: "✅".to_string(),
273                needs_update_text: "❌ {}".to_string(),
274            };
275
276            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
277            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
278
279            let parse_result = ecosystem
280                .parse_manifest(&content, &uri)
281                .await
282                .expect("Failed to parse manifest");
283
284            let doc_state = DocumentState::new_from_parse_result("npm", content, parse_result);
285            state.update_document(uri.clone(), doc_state);
286
287            let params = InlayHintParams {
288                text_document: TextDocumentIdentifier { uri },
289                work_done_progress_params: Default::default(),
290                range: tower_lsp_server::ls_types::Range::new(
291                    tower_lsp_server::ls_types::Position::new(0, 0),
292                    tower_lsp_server::ls_types::Position::new(100, 0),
293                ),
294            };
295
296            let (client, full_config) = create_test_client_and_config();
297            let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
298            // Test passes if no panic occurs
299        }
300    }
301
302    // PyPI-specific tests
303    #[cfg(feature = "pypi")]
304    mod pypi_tests {
305        use super::*;
306        use crate::document::DocumentState;
307
308        #[tokio::test]
309        async fn test_handle_inlay_hints() {
310            let state = Arc::new(ServerState::new());
311            let uri = Uri::from_file_path("/test/pyproject.toml").unwrap();
312            let config = InlayHintsConfig {
313                enabled: true,
314                up_to_date_text: "✅".to_string(),
315                needs_update_text: "❌ {}".to_string(),
316            };
317
318            let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
319            let content = r#"[project]
320dependencies = ["requests>=2.0.0"]
321"#
322            .to_string();
323
324            let parse_result = ecosystem
325                .parse_manifest(&content, &uri)
326                .await
327                .expect("Failed to parse manifest");
328
329            let doc_state = DocumentState::new_from_parse_result("pypi", content, parse_result);
330            state.update_document(uri.clone(), doc_state);
331
332            let params = InlayHintParams {
333                text_document: TextDocumentIdentifier { uri },
334                work_done_progress_params: Default::default(),
335                range: tower_lsp_server::ls_types::Range::new(
336                    tower_lsp_server::ls_types::Position::new(0, 0),
337                    tower_lsp_server::ls_types::Position::new(100, 0),
338                ),
339            };
340
341            let (client, full_config) = create_test_client_and_config();
342            let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
343            // Test passes if no panic occurs
344        }
345    }
346}