1use lsp_types::{
5 HoverParams as LspHoverParams, PartialResultParams, ReferenceContext, ReferenceParams,
6 TextDocumentIdentifier, TextDocumentPositionParams, WorkDoneProgressParams,
7};
8
9use super::Translator;
10use super::dto::{
11 DefinitionResult, HoverResult, Location, LocationsResult, Position, ReferencesResult,
12};
13use super::encoding_ctx::EncodingCtx;
14use crate::config::ToolKind;
15use crate::error::Result;
16
17fn definition_to_locations(definition: lsp_types::Definition) -> Vec<lsp_types::Location> {
19 match definition {
20 lsp_types::Definition::Location(loc) => vec![loc],
21 lsp_types::Definition::LocationList(locs) => locs,
22 }
23}
24
25fn definition_link_to_location(link: lsp_types::DefinitionLink) -> lsp_types::Location {
27 lsp_types::Location {
28 uri: link.target_uri,
29 range: link.target_selection_range,
30 }
31}
32
33async fn lsp_locations_to_mcp(locs: Vec<lsp_types::Location>, ctx: &EncodingCtx) -> Vec<Location> {
36 let mut locations = Vec::with_capacity(locs.len());
37 for loc in locs {
38 locations.push(Location {
39 uri: loc.uri.to_string(),
40 range: ctx.normalize_range(&loc.uri, loc.range).await,
41 });
42 }
43 locations
44}
45
46enum GotoKind {
51 Definition(lsp_types::Definition),
53 DefinitionLinkList(Vec<lsp_types::DefinitionLink>),
55}
56
57trait GotoResponse {
61 fn into_kind(self) -> GotoKind;
64}
65
66impl GotoResponse for lsp_types::DefinitionResponse {
67 fn into_kind(self) -> GotoKind {
68 match self {
69 Self::Definition(def) => GotoKind::Definition(def),
70 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
71 }
72 }
73}
74
75impl GotoResponse for lsp_types::ImplementationResponse {
76 fn into_kind(self) -> GotoKind {
77 match self {
78 Self::Definition(def) => GotoKind::Definition(def),
79 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
80 }
81 }
82}
83
84impl GotoResponse for lsp_types::TypeDefinitionResponse {
85 fn into_kind(self) -> GotoKind {
86 match self {
87 Self::Definition(def) => GotoKind::Definition(def),
88 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
89 }
90 }
91}
92
93async fn goto_response_to_locations<R: GotoResponse>(
97 response: Option<R>,
98 ctx: &EncodingCtx,
99) -> Vec<Location> {
100 let lsp_locs = match response.map(GotoResponse::into_kind) {
101 Some(GotoKind::Definition(def)) => definition_to_locations(def),
102 Some(GotoKind::DefinitionLinkList(links)) => {
103 links.into_iter().map(definition_link_to_location).collect()
104 }
105 None => vec![],
106 };
107 lsp_locations_to_mcp(lsp_locs, ctx).await
108}
109
110trait GotoParams: Sized {
114 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self;
117}
118
119impl GotoParams for lsp_types::DefinitionParams {
120 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
121 Self {
122 text_document_position_params,
123 work_done_progress_params: WorkDoneProgressParams::default(),
124 partial_result_params: PartialResultParams::default(),
125 }
126 }
127}
128
129impl GotoParams for lsp_types::ImplementationParams {
130 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
131 Self {
132 text_document_position_params,
133 work_done_progress_params: WorkDoneProgressParams::default(),
134 partial_result_params: PartialResultParams::default(),
135 }
136 }
137}
138
139impl GotoParams for lsp_types::TypeDefinitionParams {
140 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
141 Self {
142 text_document_position_params,
143 work_done_progress_params: WorkDoneProgressParams::default(),
144 partial_result_params: PartialResultParams::default(),
145 }
146 }
147}
148
149#[allow(deprecated)]
157fn extract_hover_contents(contents: lsp_types::Contents) -> String {
158 match contents {
159 lsp_types::Contents::MarkedString(marked_string) => marked_string_to_string(marked_string),
160 lsp_types::Contents::MarkedStringList(marked_strings) => marked_strings
161 .into_iter()
162 .map(marked_string_to_string)
163 .collect::<Vec<_>>()
164 .join("\n\n"),
165 lsp_types::Contents::MarkupContent(markup) => markup.value,
166 }
167}
168
169#[allow(deprecated)]
171fn marked_string_to_string(marked: lsp_types::MarkedString) -> String {
172 match marked {
173 lsp_types::MarkedString::String(s) => s,
174 lsp_types::MarkedString::MarkedStringWithLanguage(ls) => {
175 format!("```{}\n{}\n```", ls.language, ls.value)
176 }
177 }
178}
179
180impl Translator {
181 pub async fn handle_hover(&self, file_path: String, position: Position) -> Result<HoverResult> {
188 let Position { line, character } = position;
189 let (server_id, client, uri) = self
190 .prepare_gated_document(&file_path, ToolKind::Hover, "hoverProvider", |caps| {
191 matches!(
192 caps.hover_provider,
193 Some(
194 lsp_types::HoverProvider::Bool(true)
195 | lsp_types::HoverProvider::HoverOptions(_)
196 )
197 )
198 })
199 .await?;
200 let ctx = self.encoding_ctx(&server_id);
201 let lsp_position = ctx.to_lsp(&uri, line, character).await;
202 let response_uri = uri.clone();
203
204 let params = LspHoverParams {
205 text_document_position_params: TextDocumentPositionParams {
206 text_document: TextDocumentIdentifier { uri },
207 position: lsp_position,
208 },
209 work_done_progress_params: WorkDoneProgressParams::default(),
210 };
211
212 let response = client
213 .request_typed::<lsp_types::HoverRequest>(params, client.request_timeout())
214 .await?;
215
216 let result = match response {
217 Some(hover) => {
218 let contents = extract_hover_contents(hover.contents);
219 let range = match hover.range {
220 Some(r) => Some(ctx.normalize_range(&response_uri, r).await),
221 None => None,
222 };
223 HoverResult { contents, range }
224 }
225 None => HoverResult {
226 contents: "No hover information available".to_string(),
227 range: None,
228 },
229 };
230
231 Ok(result)
232 }
233
234 async fn handle_goto<R, T>(
246 &self,
247 file_path: &str,
248 position: Position,
249 tool: ToolKind,
250 capability: &'static str,
251 supported: impl FnOnce(&lsp_types::ServerCapabilities) -> bool,
252 ) -> Result<Vec<Location>>
253 where
254 R: lsp_types::Request<Result = Option<T>>,
255 R::Params: GotoParams,
256 T: GotoResponse,
257 {
258 let Position { line, character } = position;
259 let (server_id, client, uri) = self
260 .prepare_gated_document(file_path, tool, capability, supported)
261 .await?;
262 let ctx = self.encoding_ctx(&server_id);
263 let lsp_position = ctx.to_lsp(&uri, line, character).await;
264
265 let params = R::Params::from_position(TextDocumentPositionParams {
266 text_document: TextDocumentIdentifier { uri },
267 position: lsp_position,
268 });
269
270 let response = client
271 .request_typed::<R>(params, client.request_timeout())
272 .await?;
273
274 Ok(goto_response_to_locations(response, &ctx).await)
275 }
276
277 pub async fn handle_definition(
284 &self,
285 file_path: String,
286 position: Position,
287 ) -> Result<DefinitionResult> {
288 let locations = self
289 .handle_goto::<lsp_types::DefinitionRequest, _>(
290 &file_path,
291 position,
292 ToolKind::Definition,
293 "definitionProvider",
294 |caps| {
295 matches!(
296 caps.definition_provider,
297 Some(
298 lsp_types::DefinitionProvider::Bool(true)
299 | lsp_types::DefinitionProvider::DefinitionOptions(_)
300 )
301 )
302 },
303 )
304 .await?;
305
306 Ok(DefinitionResult { locations })
307 }
308
309 pub async fn handle_references(
316 &self,
317 file_path: String,
318 position: Position,
319 include_declaration: bool,
320 ) -> Result<ReferencesResult> {
321 let Position { line, character } = position;
322 let (server_id, client, uri) = self
323 .prepare_gated_document(
324 &file_path,
325 ToolKind::References,
326 "referencesProvider",
327 |caps| {
328 matches!(
329 caps.references_provider,
330 Some(
331 lsp_types::ReferencesProvider::Bool(true)
332 | lsp_types::ReferencesProvider::ReferenceOptions(_)
333 )
334 )
335 },
336 )
337 .await?;
338 let ctx = self.encoding_ctx(&server_id);
339 let lsp_position = ctx.to_lsp(&uri, line, character).await;
340
341 let params = ReferenceParams {
342 text_document_position_params: TextDocumentPositionParams {
343 text_document: TextDocumentIdentifier { uri },
344 position: lsp_position,
345 },
346 work_done_progress_params: WorkDoneProgressParams::default(),
347 partial_result_params: PartialResultParams::default(),
348 context: ReferenceContext {
349 include_declaration,
350 },
351 };
352
353 let response = client
354 .request_typed::<lsp_types::ReferencesRequest>(params, client.request_timeout())
355 .await?;
356
357 let locations = response.unwrap_or_default();
358
359 let mut result_locations = Vec::with_capacity(locations.len());
360 for loc in locations {
361 result_locations.push(Location {
362 uri: loc.uri.to_string(),
363 range: ctx.normalize_range(&loc.uri, loc.range).await,
364 });
365 }
366 let result = ReferencesResult {
367 locations: result_locations,
368 };
369
370 Ok(result)
371 }
372
373 pub async fn handle_implementation(
382 &self,
383 file_path: String,
384 position: Position,
385 ) -> Result<LocationsResult> {
386 let locations = self
387 .handle_goto::<lsp_types::ImplementationRequest, _>(
388 &file_path,
389 position,
390 ToolKind::Implementation,
391 "implementationProvider",
392 |caps| {
393 matches!(
394 caps.implementation_provider,
395 Some(
396 lsp_types::ImplementationProvider::Bool(true)
397 | lsp_types::ImplementationProvider::ImplementationOptions(_)
398 | lsp_types::ImplementationProvider::ImplementationRegistrationOptions(_)
399 )
400 )
401 },
402 )
403 .await?;
404
405 Ok(LocationsResult { locations })
406 }
407
408 pub async fn handle_type_definition(
418 &self,
419 file_path: String,
420 position: Position,
421 ) -> Result<LocationsResult> {
422 let locations = self
423 .handle_goto::<lsp_types::TypeDefinitionRequest, _>(
424 &file_path,
425 position,
426 ToolKind::TypeDefinition,
427 "typeDefinitionProvider",
428 |caps| {
429 matches!(
430 caps.type_definition_provider,
431 Some(
432 lsp_types::TypeDefinitionProvider::Bool(true)
433 | lsp_types::TypeDefinitionProvider::TypeDefinitionOptions(_)
434 | lsp_types::TypeDefinitionProvider::TypeDefinitionRegistrationOptions(_)
435 )
436 )
437 },
438 )
439 .await?;
440
441 Ok(LocationsResult { locations })
442 }
443}
444
445#[cfg(test)]
446#[allow(clippy::unwrap_used, clippy::expect_used, deprecated)]
447mod tests {
448 use std::fs;
449 use std::sync::Arc;
450 use std::time::Duration;
451
452 use tempfile::TempDir;
453 use tokio::io::BufReader;
454 use tokio::time::timeout;
455 use url::Url;
456
457 use super::*;
458 use crate::bridge::translator::testing::*;
459 use crate::config::ServerId;
460
461 #[test]
462 fn test_extract_hover_contents_string() {
463 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
464 let contents = lsp_types::Contents::MarkedString(marked_string);
465 let result = extract_hover_contents(contents);
466 assert_eq!(result, "Test hover");
467 }
468
469 #[test]
470 fn test_extract_hover_contents_language_string() {
471 let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
472 lsp_types::MarkedStringWithLanguage {
473 language: "rust".to_string(),
474 value: "fn main() {}".to_string(),
475 },
476 );
477 let contents = lsp_types::Contents::MarkedString(marked_string);
478 let result = extract_hover_contents(contents);
479 assert_eq!(result, "```rust\nfn main() {}\n```");
480 }
481
482 #[test]
483 fn test_extract_hover_contents_markup() {
484 let markup = lsp_types::MarkupContent {
485 kind: lsp_types::MarkupKind::Markdown,
486 value: "# Documentation".to_string(),
487 };
488 let contents = lsp_types::Contents::MarkupContent(markup);
489 let result = extract_hover_contents(contents);
490 assert_eq!(result, "# Documentation");
491 }
492
493 #[tokio::test]
497 async fn test_handle_definition_flattens_single_location() {
498 let dir = TempDir::new().unwrap();
499 let server_id = ServerId::from("rust");
500 let caps = lsp_types::ServerCapabilities {
501 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
502 ..Default::default()
503 };
504 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
505
506 let path = dir.path().join("main.rs");
507 fs::write(&path, "fn main() {}").unwrap();
508 let target_path = dir.path().join("target.rs");
509 fs::write(&target_path, "fn target() {}").unwrap();
510 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
511
512 let translator = Arc::new(translator);
513 let handle = {
514 let translator = Arc::clone(&translator);
515 let path = path.to_string_lossy().to_string();
516 tokio::spawn(async move {
517 translator
518 .handle_definition(
519 path,
520 Position {
521 line: 1,
522 character: 1,
523 },
524 )
525 .await
526 })
527 };
528
529 let mut wire = BufReader::new(&mut server.write_stdout);
530 let opened = read_framed_message(&mut wire).await;
531 assert_eq!(opened["method"], "textDocument/didOpen");
532 let request = read_framed_message(&mut wire).await;
533 assert_eq!(request["method"], "textDocument/definition");
534
535 write_response(
536 &mut server.read_half_stdin,
537 &request["id"],
538 serde_json::json!({
539 "uri": target_uri,
540 "range": {
541 "start": {"line": 0, "character": 0},
542 "end": {"line": 0, "character": 6}
543 }
544 }),
545 )
546 .await;
547
548 let result = timeout(Duration::from_secs(2), handle)
549 .await
550 .expect("handle_definition should not hang")
551 .unwrap()
552 .unwrap();
553
554 assert_eq!(result.locations.len(), 1);
555 assert_eq!(result.locations[0].uri, target_uri);
556 }
557
558 #[tokio::test]
562 async fn test_handle_implementation_flattens_location_list() {
563 let dir = TempDir::new().unwrap();
564 let server_id = ServerId::from("rust");
565 let caps = lsp_types::ServerCapabilities {
566 implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
567 ..Default::default()
568 };
569 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
570
571 let path = dir.path().join("main.rs");
572 fs::write(&path, "fn main() {}").unwrap();
573 let first_impl_path = dir.path().join("impl_a.rs");
574 fs::write(&first_impl_path, "struct A;").unwrap();
575 let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
576 let second_impl_path = dir.path().join("impl_b.rs");
577 fs::write(&second_impl_path, "struct B;").unwrap();
578 let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
579
580 let translator = Arc::new(translator);
581 let handle = {
582 let translator = Arc::clone(&translator);
583 let path = path.to_string_lossy().to_string();
584 tokio::spawn(async move {
585 translator
586 .handle_implementation(
587 path,
588 Position {
589 line: 1,
590 character: 1,
591 },
592 )
593 .await
594 })
595 };
596
597 let mut wire = BufReader::new(&mut server.write_stdout);
598 let opened = read_framed_message(&mut wire).await;
599 assert_eq!(opened["method"], "textDocument/didOpen");
600 let request = read_framed_message(&mut wire).await;
601 assert_eq!(request["method"], "textDocument/implementation");
602
603 write_response(
604 &mut server.read_half_stdin,
605 &request["id"],
606 serde_json::json!([
607 {
608 "uri": first_impl_uri,
609 "range": {
610 "start": {"line": 0, "character": 0},
611 "end": {"line": 0, "character": 9}
612 }
613 },
614 {
615 "uri": second_impl_uri,
616 "range": {
617 "start": {"line": 0, "character": 0},
618 "end": {"line": 0, "character": 9}
619 }
620 }
621 ]),
622 )
623 .await;
624
625 let result = timeout(Duration::from_secs(2), handle)
626 .await
627 .expect("handle_implementation should not hang")
628 .unwrap()
629 .unwrap();
630
631 assert_eq!(result.locations.len(), 2);
632 assert_eq!(result.locations[0].uri, first_impl_uri);
633 assert_eq!(result.locations[1].uri, second_impl_uri);
634 }
635
636 #[tokio::test]
642 async fn test_handle_type_definition_flattens_definition_link_list() {
643 let dir = TempDir::new().unwrap();
644 let server_id = ServerId::from("rust");
645 let caps = lsp_types::ServerCapabilities {
646 type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
647 ..Default::default()
648 };
649 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
650
651 let path = dir.path().join("main.rs");
652 fs::write(&path, "fn main() {}").unwrap();
653 let target_path = dir.path().join("target_type.rs");
654 fs::write(&target_path, "struct TargetType;").unwrap();
655 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
656
657 let translator = Arc::new(translator);
658 let handle = {
659 let translator = Arc::clone(&translator);
660 let path = path.to_string_lossy().to_string();
661 tokio::spawn(async move {
662 translator
663 .handle_type_definition(
664 path,
665 Position {
666 line: 1,
667 character: 1,
668 },
669 )
670 .await
671 })
672 };
673
674 let mut wire = BufReader::new(&mut server.write_stdout);
675 let opened = read_framed_message(&mut wire).await;
676 assert_eq!(opened["method"], "textDocument/didOpen");
677 let request = read_framed_message(&mut wire).await;
678 assert_eq!(request["method"], "textDocument/typeDefinition");
679
680 write_response(
681 &mut server.read_half_stdin,
682 &request["id"],
683 serde_json::json!([{
684 "targetUri": target_uri,
685 "targetRange": {
686 "start": {"line": 0, "character": 0},
687 "end": {"line": 0, "character": 18}
688 },
689 "targetSelectionRange": {
690 "start": {"line": 0, "character": 7},
691 "end": {"line": 0, "character": 17}
692 }
693 }]),
694 )
695 .await;
696
697 let result = timeout(Duration::from_secs(2), handle)
698 .await
699 .expect("handle_type_definition should not hang")
700 .unwrap()
701 .unwrap();
702
703 assert_eq!(result.locations.len(), 1);
704 assert_eq!(result.locations[0].uri, target_uri);
705 assert_eq!(result.locations[0].range.start.character, 8);
706 }
707}