Skip to main content

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 deps_core::VersionData;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8use tower_lsp_server::Client;
9use tower_lsp_server::ls_types::{Hover, HoverParams};
10
11/// Handles hover requests using trait-based delegation.
12pub async fn handle_hover(
13    state: Arc<ServerState>,
14    params: HoverParams,
15    client: Client,
16    config: Arc<RwLock<DepsConfig>>,
17) -> Option<Hover> {
18    let uri = &params.text_document_position_params.text_document.uri;
19    let position = params.text_document_position_params.position;
20
21    // Ensure document is loaded (cold start support)
22    if !ensure_document_loaded(uri, Arc::clone(&state), client, Arc::clone(&config)).await {
23        tracing::warn!("Could not load document for hover: {:?}", uri);
24        return None;
25    }
26
27    // Snapshot before the document lookup, matching diagnostics.rs's ordering — this
28    // acquires the config RwLock before the DashMap shard guard, never the reverse.
29    let (freshness, offline, supply_chain_enabled) = {
30        let config = config.read().await;
31        (
32            config.freshness.to_settings(),
33            config.network.offline,
34            config.supply_chain.enabled,
35        )
36    };
37
38    // Own everything `generate_hover` needs and release the DashMap shard `Ref`
39    // before awaiting it: the default impl awaits a real registry fetch
40    // (`Registry::get_versions_with`), so holding the guard across that await would
41    // block a concurrent `documents.get_mut` on the same shard for the duration (#319).
42    // `with_document` makes this structural rather than a convention to remember (#333).
43    let (
44        ecosystem,
45        ecosystem_id,
46        parse_result,
47        cached_versions,
48        resolved_versions,
49        vulnerabilities,
50        outcomes,
51    ) = state
52        .with_document(uri, |doc| {
53            let ecosystem = state.ecosystem_registry.get(doc.ecosystem_id())?;
54            let parse_result = doc.parse_result_arc()?;
55            Some((
56                ecosystem,
57                doc.ecosystem,
58                parse_result,
59                doc.cached_versions.clone(),
60                doc.resolved_versions.clone(),
61                doc.vulnerabilities.clone(),
62                doc.outcomes.clone(),
63            ))
64        })
65        .flatten()?;
66
67    let mut versions = VersionData::new(&cached_versions, &resolved_versions)
68        .with_vulnerabilities(&vulnerabilities)
69        .with_outcomes(&outcomes)
70        .with_ecosystem(ecosystem_id)
71        .with_offline(offline);
72    // The only call site that ever sets `VersionData::trust` (deps-core's
73    // `lsp_helpers::hover` module docs) — this is what makes the supply-chain
74    // trust signal hover-only by construction (FR-010): diagnostics, code
75    // actions, inlay hints, and code lenses never reach this code path.
76    if supply_chain_enabled {
77        versions = versions.with_trust(&state.deps_dev);
78    }
79
80    ecosystem
81        .generate_hover(parse_result.as_ref(), position, versions, freshness)
82        .await
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::document::ServerState;
89    use crate::test_utils::test_helpers::create_test_client_and_config;
90    use deps_core::EcosystemId;
91    use tower_lsp_server::ls_types::{
92        Position, TextDocumentIdentifier, TextDocumentPositionParams,
93    };
94
95    // Generic tests (no feature flag required)
96
97    #[tokio::test]
98    async fn test_handle_hover_missing_document() {
99        let state = Arc::new(ServerState::new());
100        let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
101        let (client, config) = create_test_client_and_config();
102
103        let params = HoverParams {
104            text_document_position_params: TextDocumentPositionParams {
105                text_document: TextDocumentIdentifier { uri },
106                position: Position::new(0, 0),
107            },
108            work_done_progress_params: Default::default(),
109        };
110
111        let result = handle_hover(state, params, client, config).await;
112        assert!(result.is_none());
113    }
114
115    // Cargo-specific tests
116    #[cfg(feature = "cargo")]
117    mod cargo_tests {
118        use super::*;
119        use crate::document::DocumentState;
120
121        #[tokio::test]
122        async fn test_handle_hover() {
123            let state = Arc::new(ServerState::new());
124            let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
125
126            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
127            let content = r#"[dependencies]
128serde = "1.0.0"
129"#
130            .to_string();
131
132            let parse_result = ecosystem
133                .parse_manifest(&content, &uri)
134                .await
135                .expect("Failed to parse manifest");
136
137            let doc_state =
138                DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
139            state.update_document(uri.clone(), doc_state);
140
141            let params = HoverParams {
142                text_document_position_params: TextDocumentPositionParams {
143                    text_document: TextDocumentIdentifier { uri },
144                    position: Position::new(1, 0),
145                },
146                work_done_progress_params: Default::default(),
147            };
148
149            let (client, config) = create_test_client_and_config();
150            let _result = handle_hover(state, params, client, config).await;
151            // Test passes if no panic occurs
152        }
153
154        #[tokio::test]
155        async fn test_handle_hover_no_parse_result() {
156            let state = Arc::new(ServerState::new());
157            let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
158
159            let doc_state =
160                DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
161            state.update_document(uri.clone(), doc_state);
162
163            let params = HoverParams {
164                text_document_position_params: TextDocumentPositionParams {
165                    text_document: TextDocumentIdentifier { uri },
166                    position: Position::new(0, 0),
167                },
168                work_done_progress_params: Default::default(),
169            };
170
171            let (client, config) = create_test_client_and_config();
172            let result = handle_hover(state, params, client, config).await;
173            assert!(result.is_none());
174        }
175    }
176
177    // npm-specific tests
178    #[cfg(feature = "npm")]
179    mod npm_tests {
180        use super::*;
181        use crate::document::DocumentState;
182
183        #[tokio::test]
184        async fn test_handle_hover() {
185            let state = Arc::new(ServerState::new());
186            let uri = deps_core::test_util::test_uri("/test/package.json");
187
188            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
189            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
190
191            let parse_result = ecosystem
192                .parse_manifest(&content, &uri)
193                .await
194                .expect("Failed to parse manifest");
195
196            let doc_state =
197                DocumentState::new_from_parse_result(EcosystemId::Npm, content, parse_result);
198            state.update_document(uri.clone(), doc_state);
199
200            let params = HoverParams {
201                text_document_position_params: TextDocumentPositionParams {
202                    text_document: TextDocumentIdentifier { uri },
203                    position: Position::new(0, 20),
204                },
205                work_done_progress_params: Default::default(),
206            };
207
208            let (client, config) = create_test_client_and_config();
209            let _result = handle_hover(state, params, client, config).await;
210            // Test passes if no panic occurs
211        }
212    }
213}