deps_lsp/handlers/
hover.rs

1//! Hover handler using ecosystem trait delegation.
2
3use crate::config::DepsConfig;
4use crate::document::{ServerState, ensure_document_loaded};
5use std::sync::Arc;
6use tokio::sync::RwLock;
7use tower_lsp_server::Client;
8use tower_lsp_server::ls_types::{Hover, HoverParams};
9
10/// Handles hover requests using trait-based delegation.
11pub async fn handle_hover(
12    state: Arc<ServerState>,
13    params: HoverParams,
14    client: Client,
15    config: Arc<RwLock<DepsConfig>>,
16) -> Option<Hover> {
17    let uri = &params.text_document_position_params.text_document.uri;
18    let position = params.text_document_position_params.position;
19
20    // Ensure document is loaded (cold start support)
21    if !ensure_document_loaded(uri, Arc::clone(&state), client, config).await {
22        tracing::warn!("Could not load document for hover: {:?}", uri);
23        return None;
24    }
25
26    // Single document lookup: extract all needed data at once
27    let doc = state.get_document(uri)?;
28    let ecosystem = state.ecosystem_registry.get(doc.ecosystem_id)?;
29    let parse_result = doc.parse_result()?;
30
31    // Generate hover while holding the lock
32    ecosystem
33        .generate_hover(
34            parse_result,
35            position,
36            &doc.cached_versions,
37            &doc.resolved_versions,
38        )
39        .await
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::document::ServerState;
46    use crate::test_utils::test_helpers::create_test_client_and_config;
47    use tower_lsp_server::ls_types::{
48        Position, TextDocumentIdentifier, TextDocumentPositionParams, Uri,
49    };
50
51    // Generic tests (no feature flag required)
52
53    #[tokio::test]
54    async fn test_handle_hover_missing_document() {
55        let state = Arc::new(ServerState::new());
56        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
57        let (client, config) = create_test_client_and_config();
58
59        let params = HoverParams {
60            text_document_position_params: TextDocumentPositionParams {
61                text_document: TextDocumentIdentifier { uri },
62                position: Position::new(0, 0),
63            },
64            work_done_progress_params: Default::default(),
65        };
66
67        let result = handle_hover(state, params, client, config).await;
68        assert!(result.is_none());
69    }
70
71    // Cargo-specific tests
72    #[cfg(feature = "cargo")]
73    mod cargo_tests {
74        use super::*;
75        use crate::document::{DocumentState, Ecosystem};
76
77        #[tokio::test]
78        async fn test_handle_hover() {
79            let state = Arc::new(ServerState::new());
80            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
81
82            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
83            let content = r#"[dependencies]
84serde = "1.0.0"
85"#
86            .to_string();
87
88            let parse_result = ecosystem
89                .parse_manifest(&content, &uri)
90                .await
91                .expect("Failed to parse manifest");
92
93            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
94            state.update_document(uri.clone(), doc_state);
95
96            let params = HoverParams {
97                text_document_position_params: TextDocumentPositionParams {
98                    text_document: TextDocumentIdentifier { uri },
99                    position: Position::new(1, 0),
100                },
101                work_done_progress_params: Default::default(),
102            };
103
104            let (client, config) = create_test_client_and_config();
105            let _result = handle_hover(state, params, client, config).await;
106            // Test passes if no panic occurs
107        }
108
109        #[tokio::test]
110        async fn test_handle_hover_no_parse_result() {
111            let state = Arc::new(ServerState::new());
112            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
113
114            let doc_state = DocumentState::new(Ecosystem::Cargo, String::new(), vec![]);
115            state.update_document(uri.clone(), doc_state);
116
117            let params = HoverParams {
118                text_document_position_params: TextDocumentPositionParams {
119                    text_document: TextDocumentIdentifier { uri },
120                    position: Position::new(0, 0),
121                },
122                work_done_progress_params: Default::default(),
123            };
124
125            let (client, config) = create_test_client_and_config();
126            let result = handle_hover(state, params, client, config).await;
127            assert!(result.is_none());
128        }
129    }
130
131    // npm-specific tests
132    #[cfg(feature = "npm")]
133    mod npm_tests {
134        use super::*;
135        use crate::document::DocumentState;
136
137        #[tokio::test]
138        async fn test_handle_hover() {
139            let state = Arc::new(ServerState::new());
140            let uri = Uri::from_file_path("/test/package.json").unwrap();
141
142            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
143            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
144
145            let parse_result = ecosystem
146                .parse_manifest(&content, &uri)
147                .await
148                .expect("Failed to parse manifest");
149
150            let doc_state = DocumentState::new_from_parse_result("npm", content, parse_result);
151            state.update_document(uri.clone(), doc_state);
152
153            let params = HoverParams {
154                text_document_position_params: TextDocumentPositionParams {
155                    text_document: TextDocumentIdentifier { uri },
156                    position: Position::new(0, 20),
157                },
158                work_done_progress_params: Default::default(),
159            };
160
161            let (client, config) = create_test_client_and_config();
162            let _result = handle_hover(state, params, client, config).await;
163            // Test passes if no panic occurs
164        }
165    }
166}