1use crate::config::{DepsConfig, InlayHintsConfig};
7use crate::document::{ServerState, ensure_document_loaded};
8use deps_core::{EcosystemConfig, VersionData};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tower_lsp_server::Client;
12use tower_lsp_server::ls_types::{InlayHint, InlayHintParams};
13
14pub 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 = ¶ms.text_document.uri;
30
31 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 let (loading_config, offline) = {
39 let full_config = full_config.read().await;
40 (
41 full_config.loading_indicator.clone(),
42 full_config.network.offline,
43 )
44 };
45
46 let Some(extracted) = state.with_document(uri, |doc| {
50 let Some(ecosystem) = state.ecosystem_registry.get(doc.ecosystem_id()) else {
51 tracing::warn!("Ecosystem not found: {}", doc.ecosystem_id());
52 return None;
53 };
54 let parse_result = doc.parse_result_arc()?;
55 Some((
56 ecosystem,
57 parse_result,
58 doc.cached_versions.clone(),
59 doc.resolved_versions.clone(),
60 doc.loading_state,
61 ))
62 }) else {
63 tracing::warn!("Document not found: {:?}", uri);
64 return vec![];
65 };
66
67 let Some((ecosystem, parse_result, cached_versions, resolved_versions, loading_state)) =
68 extracted
69 else {
70 return vec![];
71 };
72
73 let ecosystem_config = EcosystemConfig {
74 show_up_to_date_hints: true,
75 up_to_date_text: config.up_to_date_text.clone(),
76 needs_update_text: config.needs_update_text.clone(),
77 loading_text: loading_config.loading_text,
78 show_loading_hints: loading_config.enabled && loading_config.fallback_to_hints,
79 offline,
80 };
81
82 ecosystem
83 .generate_inlay_hints(
84 parse_result.as_ref(),
85 VersionData::new(&cached_versions, &resolved_versions),
86 loading_state,
87 &ecosystem_config,
88 )
89 .await
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::document::ServerState;
96 use crate::test_utils::test_helpers::create_test_client_and_config;
97 use deps_core::EcosystemId;
98 use tower_lsp_server::ls_types::TextDocumentIdentifier;
99
100 #[test]
103 fn test_handle_inlay_hints_disabled() {
104 let config = InlayHintsConfig {
105 enabled: false,
106 up_to_date_text: "✅".to_string(),
107 needs_update_text: "❌ {}".to_string(),
108 };
109
110 assert!(!config.enabled);
111 }
112
113 #[tokio::test]
114 async fn test_handle_inlay_hints_disabled_returns_empty() {
115 let state = Arc::new(ServerState::new());
116 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
117 let config = InlayHintsConfig {
118 enabled: false,
119 up_to_date_text: "✅".to_string(),
120 needs_update_text: "❌ {}".to_string(),
121 };
122
123 let params = InlayHintParams {
124 text_document: TextDocumentIdentifier { uri },
125 work_done_progress_params: Default::default(),
126 range: tower_lsp_server::ls_types::Range::new(
127 tower_lsp_server::ls_types::Position::new(0, 0),
128 tower_lsp_server::ls_types::Position::new(100, 0),
129 ),
130 };
131
132 let (client, full_config) = create_test_client_and_config();
133 let result = handle_inlay_hints(state, params, &config, client, full_config).await;
134 assert!(result.is_empty());
135 }
136
137 #[tokio::test]
138 async fn test_handle_inlay_hints_missing_document() {
139 let state = Arc::new(ServerState::new());
140 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
141 let config = InlayHintsConfig {
142 enabled: true,
143 up_to_date_text: "✅".to_string(),
144 needs_update_text: "❌ {}".to_string(),
145 };
146
147 let params = InlayHintParams {
148 text_document: TextDocumentIdentifier { uri },
149 work_done_progress_params: Default::default(),
150 range: tower_lsp_server::ls_types::Range::new(
151 tower_lsp_server::ls_types::Position::new(0, 0),
152 tower_lsp_server::ls_types::Position::new(100, 0),
153 ),
154 };
155
156 let (client, full_config) = create_test_client_and_config();
157 let result = handle_inlay_hints(state, params, &config, client, full_config).await;
158 assert!(result.is_empty());
159 }
160
161 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
173 async fn test_concurrent_document_write_not_blocked_by_in_flight_inlay_hints() {
174 use crate::test_utils::blocking_ecosystem::{
175 BlockingEcosystem, BlockingHook, MockParseResult,
176 };
177 use deps_core::ParseResult;
178 use tokio::sync::Barrier;
179
180 let state = Arc::new(ServerState::new());
181 let started = Arc::new(Barrier::new(2));
182 state
183 .ecosystem_registry
184 .register(Arc::new(BlockingEcosystem {
185 started: Arc::clone(&started),
186 hook: BlockingHook::InlayHints,
187 }));
188
189 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
190 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
191 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
192 let doc = crate::document::DocumentState::new_from_parse_result(
193 EcosystemId::Cargo,
194 content,
195 parse_result,
196 );
197 state.update_document(uri.clone(), doc);
198
199 let config = InlayHintsConfig {
200 enabled: true,
201 up_to_date_text: "up to date".to_string(),
202 needs_update_text: "outdated: {}".to_string(),
203 };
204 let (client, full_config) = create_test_client_and_config();
205
206 let handler_task = tokio::spawn({
207 let state = Arc::clone(&state);
208 let uri = uri.clone();
209 async move {
210 let params = InlayHintParams {
211 text_document: TextDocumentIdentifier { uri },
212 work_done_progress_params: Default::default(),
213 range: tower_lsp_server::ls_types::Range::new(
214 tower_lsp_server::ls_types::Position::new(0, 0),
215 tower_lsp_server::ls_types::Position::new(100, 0),
216 ),
217 };
218 handle_inlay_hints(state, params, &config, client, full_config).await
219 }
220 });
221
222 tokio::time::timeout(std::time::Duration::from_secs(5), started.wait())
227 .await
228 .expect("handle_inlay_hints did not reach generate_inlay_hints within 5s");
229
230 let write_task = tokio::spawn({
234 let state = Arc::clone(&state);
235 let uri = uri.clone();
236 async move {
237 state.documents.get_mut(&uri).unwrap().set_loading();
238 }
239 });
240 let write_result =
241 tokio::time::timeout(std::time::Duration::from_millis(500), write_task).await;
242
243 handler_task.abort();
244
245 assert!(
246 write_result.is_ok(),
247 "#333 regression: a concurrent documents.get_mut on the same URI must not \
248 block on an in-flight generate_inlay_hints call — the DashMap shard Ref \
249 must be dropped before the call is awaited, not after it"
250 );
251 }
252
253 #[cfg(feature = "cargo")]
255 mod cargo_tests {
256 use super::*;
257 use crate::document::DocumentState;
258
259 #[tokio::test]
260 async fn test_handle_inlay_hints() {
261 let state = Arc::new(ServerState::new());
262 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
263 let config = InlayHintsConfig {
264 enabled: true,
265 up_to_date_text: "✅".to_string(),
266 needs_update_text: "❌ {}".to_string(),
267 };
268
269 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
270 let content = r#"[dependencies]
271serde = "1.0.0"
272"#
273 .to_string();
274
275 let parse_result = ecosystem
276 .parse_manifest(&content, &uri)
277 .await
278 .expect("Failed to parse manifest");
279
280 let doc_state =
281 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
282 state.update_document(uri.clone(), doc_state);
283
284 let params = InlayHintParams {
285 text_document: TextDocumentIdentifier { uri },
286 work_done_progress_params: Default::default(),
287 range: tower_lsp_server::ls_types::Range::new(
288 tower_lsp_server::ls_types::Position::new(0, 0),
289 tower_lsp_server::ls_types::Position::new(100, 0),
290 ),
291 };
292
293 let (client, full_config) = create_test_client_and_config();
294 let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
295 }
297
298 #[tokio::test]
299 async fn test_handle_inlay_hints_no_parse_result() {
300 let state = Arc::new(ServerState::new());
301 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
302 let config = InlayHintsConfig {
303 enabled: true,
304 up_to_date_text: "✅".to_string(),
305 needs_update_text: "❌ {}".to_string(),
306 };
307
308 let doc_state =
309 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
310 state.update_document(uri.clone(), doc_state);
311
312 let params = InlayHintParams {
313 text_document: TextDocumentIdentifier { uri },
314 work_done_progress_params: Default::default(),
315 range: tower_lsp_server::ls_types::Range::new(
316 tower_lsp_server::ls_types::Position::new(0, 0),
317 tower_lsp_server::ls_types::Position::new(100, 0),
318 ),
319 };
320
321 let (client, full_config) = create_test_client_and_config();
322 let result = handle_inlay_hints(state, params, &config, client, full_config).await;
323 assert!(result.is_empty());
324 }
325
326 #[tokio::test]
327 async fn test_handle_inlay_hints_custom_config() {
328 let state = Arc::new(ServerState::new());
329 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
330 let config = InlayHintsConfig {
331 enabled: true,
332 up_to_date_text: "OK".to_string(),
333 needs_update_text: "UPDATE: {}".to_string(),
334 };
335
336 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
337 let content = r#"[dependencies]
338serde = "1.0.0"
339"#
340 .to_string();
341
342 let parse_result = ecosystem
343 .parse_manifest(&content, &uri)
344 .await
345 .expect("Failed to parse manifest");
346
347 let doc_state =
348 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
349 state.update_document(uri.clone(), doc_state);
350
351 let params = InlayHintParams {
352 text_document: TextDocumentIdentifier { uri },
353 work_done_progress_params: Default::default(),
354 range: tower_lsp_server::ls_types::Range::new(
355 tower_lsp_server::ls_types::Position::new(0, 0),
356 tower_lsp_server::ls_types::Position::new(100, 0),
357 ),
358 };
359
360 let (client, full_config) = create_test_client_and_config();
361 let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
362 }
364 }
365
366 #[cfg(feature = "npm")]
368 mod npm_tests {
369 use super::*;
370 use crate::document::DocumentState;
371
372 #[tokio::test]
373 async fn test_handle_inlay_hints() {
374 let state = Arc::new(ServerState::new());
375 let uri = deps_core::test_util::test_uri("/test/package.json");
376 let config = InlayHintsConfig {
377 enabled: true,
378 up_to_date_text: "✅".to_string(),
379 needs_update_text: "❌ {}".to_string(),
380 };
381
382 let ecosystem = state.ecosystem_registry.get("npm").unwrap();
383 let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
384
385 let parse_result = ecosystem
386 .parse_manifest(&content, &uri)
387 .await
388 .expect("Failed to parse manifest");
389
390 let doc_state =
391 DocumentState::new_from_parse_result(EcosystemId::Npm, content, parse_result);
392 state.update_document(uri.clone(), doc_state);
393
394 let params = InlayHintParams {
395 text_document: TextDocumentIdentifier { uri },
396 work_done_progress_params: Default::default(),
397 range: tower_lsp_server::ls_types::Range::new(
398 tower_lsp_server::ls_types::Position::new(0, 0),
399 tower_lsp_server::ls_types::Position::new(100, 0),
400 ),
401 };
402
403 let (client, full_config) = create_test_client_and_config();
404 let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
405 }
407 }
408
409 #[cfg(feature = "pypi")]
411 mod pypi_tests {
412 use super::*;
413 use crate::document::DocumentState;
414
415 #[tokio::test]
416 async fn test_handle_inlay_hints() {
417 let state = Arc::new(ServerState::new());
418 let uri = deps_core::test_util::test_uri("/test/pyproject.toml");
419 let config = InlayHintsConfig {
420 enabled: true,
421 up_to_date_text: "✅".to_string(),
422 needs_update_text: "❌ {}".to_string(),
423 };
424
425 let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
426 let content = r#"[project]
427dependencies = ["requests>=2.0.0"]
428"#
429 .to_string();
430
431 let parse_result = ecosystem
432 .parse_manifest(&content, &uri)
433 .await
434 .expect("Failed to parse manifest");
435
436 let doc_state =
437 DocumentState::new_from_parse_result(EcosystemId::Pypi, content, parse_result);
438 state.update_document(uri.clone(), doc_state);
439
440 let params = InlayHintParams {
441 text_document: TextDocumentIdentifier { uri },
442 work_done_progress_params: Default::default(),
443 range: tower_lsp_server::ls_types::Range::new(
444 tower_lsp_server::ls_types::Position::new(0, 0),
445 tower_lsp_server::ls_types::Position::new(100, 0),
446 ),
447 };
448
449 let (client, full_config) = create_test_client_and_config();
450 let _result = handle_inlay_hints(state, params, &config, client, full_config).await;
451 }
453 }
454}