deps_lsp/handlers/
code_actions.rs

1//! Code actions 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::{CodeActionOrCommand, CodeActionParams};
9
10/// Handles code action requests using trait-based delegation.
11pub async fn handle_code_actions(
12    state: Arc<ServerState>,
13    params: CodeActionParams,
14    client: Client,
15    config: Arc<RwLock<DepsConfig>>,
16) -> Vec<CodeActionOrCommand> {
17    let uri = &params.text_document.uri;
18    let position = params.range.start;
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 code actions: {:?}", uri);
23        return vec![];
24    }
25
26    // Single document lookup: extract all needed data at once
27    let doc = match state.get_document(uri) {
28        Some(d) => d,
29        None => return vec![],
30    };
31
32    let ecosystem = match state.ecosystem_registry.get(doc.ecosystem_id) {
33        Some(e) => e,
34        None => return vec![],
35    };
36
37    let parse_result = match doc.parse_result() {
38        Some(p) => p,
39        None => return vec![],
40    };
41
42    // Generate code actions while holding the lock
43    let actions = ecosystem
44        .generate_code_actions(parse_result, position, &doc.cached_versions, uri)
45        .await;
46
47    actions
48        .into_iter()
49        .map(CodeActionOrCommand::CodeAction)
50        .collect()
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::document::ServerState;
57    use crate::test_utils::test_helpers::create_test_client_and_config;
58    use tower_lsp_server::ls_types::{Position, Range, TextDocumentIdentifier, Uri};
59
60    // Generic tests (no feature flag required)
61
62    #[tokio::test]
63    async fn test_handle_code_actions_missing_document() {
64        let state = Arc::new(ServerState::new());
65        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
66
67        let params = CodeActionParams {
68            text_document: TextDocumentIdentifier { uri },
69            range: Range::new(Position::new(0, 0), Position::new(0, 0)),
70            context: Default::default(),
71            work_done_progress_params: Default::default(),
72            partial_result_params: Default::default(),
73        };
74
75        let (client, config) = create_test_client_and_config();
76        let result = handle_code_actions(state, params, client, config).await;
77        assert!(result.is_empty());
78    }
79
80    // Cargo-specific tests
81    #[cfg(feature = "cargo")]
82    mod cargo_tests {
83        use super::*;
84        use crate::document::{DocumentState, Ecosystem};
85
86        #[tokio::test]
87        async fn test_handle_code_actions() {
88            let state = Arc::new(ServerState::new());
89            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
90
91            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
92            let content = r#"[dependencies]
93serde = "1.0.0"
94"#
95            .to_string();
96
97            let parse_result = ecosystem
98                .parse_manifest(&content, &uri)
99                .await
100                .expect("Failed to parse manifest");
101
102            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
103            state.update_document(uri.clone(), doc_state);
104
105            let params = CodeActionParams {
106                text_document: TextDocumentIdentifier { uri },
107                range: Range::new(Position::new(1, 9), Position::new(1, 16)),
108                context: Default::default(),
109                work_done_progress_params: Default::default(),
110                partial_result_params: Default::default(),
111            };
112
113            let (client, config) = create_test_client_and_config();
114            let _result = handle_code_actions(state, params, client, config).await;
115            // Test passes if no panic occurs
116        }
117
118        #[tokio::test]
119        async fn test_handle_code_actions_no_parse_result() {
120            let state = Arc::new(ServerState::new());
121            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
122
123            let doc_state = DocumentState::new(Ecosystem::Cargo, String::new(), vec![]);
124            state.update_document(uri.clone(), doc_state);
125
126            let params = CodeActionParams {
127                text_document: TextDocumentIdentifier { uri },
128                range: Range::new(Position::new(0, 0), Position::new(0, 0)),
129                context: Default::default(),
130                work_done_progress_params: Default::default(),
131                partial_result_params: Default::default(),
132            };
133
134            let (client, config) = create_test_client_and_config();
135            let result = handle_code_actions(state, params, client, config).await;
136            assert!(result.is_empty());
137        }
138    }
139
140    // npm-specific tests
141    #[cfg(feature = "npm")]
142    mod npm_tests {
143        use super::*;
144        use crate::document::DocumentState;
145
146        #[tokio::test]
147        async fn test_handle_code_actions() {
148            let state = Arc::new(ServerState::new());
149            let uri = Uri::from_file_path("/test/package.json").unwrap();
150
151            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
152            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
153
154            let parse_result = ecosystem
155                .parse_manifest(&content, &uri)
156                .await
157                .expect("Failed to parse manifest");
158
159            let doc_state = DocumentState::new_from_parse_result("npm", content, parse_result);
160            state.update_document(uri.clone(), doc_state);
161
162            let params = CodeActionParams {
163                text_document: TextDocumentIdentifier { uri },
164                range: Range::new(Position::new(0, 25), Position::new(0, 32)),
165                context: Default::default(),
166                work_done_progress_params: Default::default(),
167                partial_result_params: Default::default(),
168            };
169
170            let (client, config) = create_test_client_and_config();
171            let _result = handle_code_actions(state, params, client, config).await;
172            // Test passes if no panic occurs
173        }
174    }
175}