Skip to main content

deps_lsp/handlers/
document_link.rs

1//! Document link handler using ecosystem trait delegation.
2//!
3//! Purely local (no registry access, no `versions`/`freshness` inputs) — see
4//! `Ecosystem::generate_document_links`, whose default is an empty `Vec` for
5//! every ecosystem that has no intra-manifest file references. PyPI is
6//! currently the only implementor, surfacing pip's `-r`/`-c` targets.
7
8use crate::config::DepsConfig;
9use crate::document::{ServerState, ensure_document_loaded};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12use tower_lsp_server::Client;
13use tower_lsp_server::ls_types::{DocumentLink, DocumentLinkParams};
14
15/// Handles `textDocument/documentLink` requests using trait-based delegation.
16pub async fn handle_document_link(
17    state: Arc<ServerState>,
18    params: DocumentLinkParams,
19    client: Client,
20    config: Arc<RwLock<DepsConfig>>,
21) -> Vec<DocumentLink> {
22    let uri = &params.text_document.uri;
23
24    // Ensure document is loaded (cold start support)
25    if !ensure_document_loaded(uri, Arc::clone(&state), client, config).await {
26        tracing::warn!("Could not load document for document link: {:?}", uri);
27        return vec![];
28    }
29
30    // Release the DashMap shard `Ref` before calling out to the ecosystem (#333):
31    // `generate_document_links` is synchronous today, but keeping the same
32    // own-then-release shape as the other handlers avoids reintroducing the
33    // shard-blocking bug if it ever grows an async fast path.
34    let Some((ecosystem, parse_result)) = state
35        .with_document(uri, |doc| {
36            let ecosystem = state.ecosystem_registry.get(doc.ecosystem_id())?;
37            let parse_result = doc.parse_result_arc()?;
38            Some((ecosystem, parse_result))
39        })
40        .flatten()
41    else {
42        return vec![];
43    };
44
45    ecosystem.generate_document_links(parse_result.as_ref(), uri)
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::document::ServerState;
52    use crate::test_utils::test_helpers::create_test_client_and_config;
53    use tower_lsp_server::ls_types::TextDocumentIdentifier;
54
55    #[tokio::test]
56    async fn test_handle_document_link_missing_document() {
57        let state = Arc::new(ServerState::new());
58        let uri = deps_core::test_util::test_uri("/test/requirements.txt");
59        let (client, config) = create_test_client_and_config();
60
61        let params = DocumentLinkParams {
62            text_document: TextDocumentIdentifier { uri },
63            work_done_progress_params: Default::default(),
64            partial_result_params: Default::default(),
65        };
66
67        let result = handle_document_link(state, params, client, config).await;
68        assert!(result.is_empty());
69    }
70
71    #[cfg(feature = "pypi")]
72    mod pypi_tests {
73        use super::*;
74        use crate::document::DocumentState;
75        use deps_core::EcosystemId;
76
77        #[tokio::test]
78        async fn test_handle_document_link_requirements_reference() {
79            let state = Arc::new(ServerState::new());
80            let uri = deps_core::test_util::test_uri("/test/requirements.txt");
81
82            let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
83            let content = "-r other-requirements.txt\nrequests==2.31.0\n".to_string();
84
85            let parse_result = ecosystem
86                .parse_manifest(&content, &uri)
87                .await
88                .expect("Failed to parse manifest");
89
90            let doc_state =
91                DocumentState::new_from_parse_result(EcosystemId::Pypi, content, parse_result);
92            state.update_document(uri.clone(), doc_state);
93
94            let params = DocumentLinkParams {
95                text_document: TextDocumentIdentifier { uri },
96                work_done_progress_params: Default::default(),
97                partial_result_params: Default::default(),
98            };
99
100            let (client, config) = create_test_client_and_config();
101            let links = handle_document_link(state, params, client, config).await;
102
103            assert_eq!(links.len(), 1);
104            let target = links[0].target.as_ref().expect("link must have a target");
105            assert!(
106                target
107                    .path()
108                    .as_str()
109                    .ends_with("/test/other-requirements.txt")
110            );
111        }
112    }
113}