Skip to main content

mcpls_core/bridge/translator/
navigation.rs

1//! Hover, go-to-definition/implementation/type-definition, and references
2//! handlers.
3
4use lsp_types::{
5    GotoDefinitionParams, Hover, HoverContents, HoverParams as LspHoverParams, MarkedString,
6    PartialResultParams, ReferenceContext, ReferenceParams, TextDocumentIdentifier,
7    TextDocumentPositionParams, WorkDoneProgressParams,
8};
9
10use super::Translator;
11use super::dto::{DefinitionResult, HoverResult, Location, LocationsResult, ReferencesResult};
12use super::encoding_ctx::EncodingCtx;
13use crate::config::ToolKind;
14use crate::error::Result;
15
16/// Normalize a `GotoDefinitionResponse` into a flat list of MCP `Location` values.
17async fn goto_response_to_locations(
18    response: Option<lsp_types::GotoDefinitionResponse>,
19    ctx: &EncodingCtx,
20) -> Vec<Location> {
21    let lsp_locs: Vec<lsp_types::Location> = match response {
22        Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
23        Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
24        Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
25            .into_iter()
26            .map(|link| lsp_types::Location {
27                uri: link.target_uri,
28                range: link.target_selection_range,
29            })
30            .collect(),
31        None => vec![],
32    };
33
34    let mut locations = Vec::with_capacity(lsp_locs.len());
35    for loc in lsp_locs {
36        locations.push(Location {
37            uri: loc.uri.to_string(),
38            range: ctx.normalize_range(&loc.uri, loc.range).await,
39        });
40    }
41    locations
42}
43
44fn extract_hover_contents(contents: HoverContents) -> String {
45    match contents {
46        HoverContents::Scalar(marked_string) => marked_string_to_string(marked_string),
47        HoverContents::Array(marked_strings) => marked_strings
48            .into_iter()
49            .map(marked_string_to_string)
50            .collect::<Vec<_>>()
51            .join("\n\n"),
52        HoverContents::Markup(markup) => markup.value,
53    }
54}
55
56/// Convert a marked string to a plain string.
57fn marked_string_to_string(marked: MarkedString) -> String {
58    match marked {
59        MarkedString::String(s) => s,
60        MarkedString::LanguageString(ls) => format!("```{}\n{}\n```", ls.language, ls.value),
61    }
62}
63
64impl Translator {
65    /// Handle hover request.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the LSP request fails, the file cannot be opened,
70    /// or the routed server does not advertise `hoverProvider` support.
71    pub async fn handle_hover(
72        &self,
73        file_path: String,
74        line: u32,
75        character: u32,
76    ) -> Result<HoverResult> {
77        let (server_id, client, uri) = self
78            .prepare_gated_document(&file_path, ToolKind::Hover, "hoverProvider", |caps| {
79                matches!(
80                    caps.hover_provider,
81                    Some(
82                        lsp_types::HoverProviderCapability::Simple(true)
83                            | lsp_types::HoverProviderCapability::Options(_)
84                    )
85                )
86            })
87            .await?;
88        let ctx = self.encoding_ctx(&server_id);
89        let lsp_position = ctx.to_lsp(&uri, line, character).await;
90        let response_uri = uri.clone();
91
92        let params = LspHoverParams {
93            text_document_position_params: TextDocumentPositionParams {
94                text_document: TextDocumentIdentifier { uri },
95                position: lsp_position,
96            },
97            work_done_progress_params: WorkDoneProgressParams::default(),
98        };
99
100        let response: Option<Hover> = client
101            .request("textDocument/hover", params, client.request_timeout())
102            .await?;
103
104        let result = match response {
105            Some(hover) => {
106                let contents = extract_hover_contents(hover.contents);
107                let range = match hover.range {
108                    Some(r) => Some(ctx.normalize_range(&response_uri, r).await),
109                    None => None,
110                };
111                HoverResult { contents, range }
112            }
113            None => HoverResult {
114                contents: "No hover information available".to_string(),
115                range: None,
116            },
117        };
118
119        Ok(result)
120    }
121
122    /// Handle definition request.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the LSP request fails, the file cannot be opened,
127    /// or the routed server does not advertise `definitionProvider` support.
128    pub async fn handle_definition(
129        &self,
130        file_path: String,
131        line: u32,
132        character: u32,
133    ) -> Result<DefinitionResult> {
134        let (server_id, client, uri) = self
135            .prepare_gated_document(
136                &file_path,
137                ToolKind::Definition,
138                "definitionProvider",
139                |caps| {
140                    matches!(
141                        caps.definition_provider,
142                        Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
143                    )
144                },
145            )
146            .await?;
147        let ctx = self.encoding_ctx(&server_id);
148        let lsp_position = ctx.to_lsp(&uri, line, character).await;
149
150        let params = GotoDefinitionParams {
151            text_document_position_params: TextDocumentPositionParams {
152                text_document: TextDocumentIdentifier { uri },
153                position: lsp_position,
154            },
155            work_done_progress_params: WorkDoneProgressParams::default(),
156            partial_result_params: PartialResultParams::default(),
157        };
158
159        let response: Option<lsp_types::GotoDefinitionResponse> = client
160            .request("textDocument/definition", params, client.request_timeout())
161            .await?;
162
163        let result = DefinitionResult {
164            locations: goto_response_to_locations(response, &ctx).await,
165        };
166
167        Ok(result)
168    }
169
170    /// Handle references request.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the LSP request fails, the file cannot be opened,
175    /// or the routed server does not advertise `referencesProvider` support.
176    pub async fn handle_references(
177        &self,
178        file_path: String,
179        line: u32,
180        character: u32,
181        include_declaration: bool,
182    ) -> Result<ReferencesResult> {
183        let (server_id, client, uri) = self
184            .prepare_gated_document(
185                &file_path,
186                ToolKind::References,
187                "referencesProvider",
188                |caps| {
189                    matches!(
190                        caps.references_provider,
191                        Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
192                    )
193                },
194            )
195            .await?;
196        let ctx = self.encoding_ctx(&server_id);
197        let lsp_position = ctx.to_lsp(&uri, line, character).await;
198
199        let params = ReferenceParams {
200            text_document_position: TextDocumentPositionParams {
201                text_document: TextDocumentIdentifier { uri },
202                position: lsp_position,
203            },
204            work_done_progress_params: WorkDoneProgressParams::default(),
205            partial_result_params: PartialResultParams::default(),
206            context: ReferenceContext {
207                include_declaration,
208            },
209        };
210
211        let response: Option<Vec<lsp_types::Location>> = client
212            .request("textDocument/references", params, client.request_timeout())
213            .await?;
214
215        let locations = response.unwrap_or_default();
216
217        let mut result_locations = Vec::with_capacity(locations.len());
218        for loc in locations {
219            result_locations.push(Location {
220                uri: loc.uri.to_string(),
221                range: ctx.normalize_range(&loc.uri, loc.range).await,
222            });
223        }
224        let result = ReferencesResult {
225            locations: result_locations,
226        };
227
228        Ok(result)
229    }
230
231    /// Handle go-to-implementation request (`textDocument/implementation`).
232    ///
233    /// Returns the locations of trait method or interface member implementations.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the LSP request fails, the file cannot be opened,
238    /// or the routed server does not advertise `implementationProvider` support.
239    pub async fn handle_implementation(
240        &self,
241        file_path: String,
242        line: u32,
243        character: u32,
244    ) -> Result<LocationsResult> {
245        let (server_id, client, uri) = self
246            .prepare_gated_document(
247                &file_path,
248                ToolKind::Implementation,
249                "implementationProvider",
250                |caps| {
251                    matches!(
252                        caps.implementation_provider,
253                        Some(
254                            lsp_types::ImplementationProviderCapability::Simple(true)
255                                | lsp_types::ImplementationProviderCapability::Options(_)
256                        )
257                    )
258                },
259            )
260            .await?;
261        let ctx = self.encoding_ctx(&server_id);
262        let lsp_position = ctx.to_lsp(&uri, line, character).await;
263
264        let params = GotoDefinitionParams {
265            text_document_position_params: TextDocumentPositionParams {
266                text_document: TextDocumentIdentifier { uri },
267                position: lsp_position,
268            },
269            work_done_progress_params: WorkDoneProgressParams::default(),
270            partial_result_params: PartialResultParams::default(),
271        };
272
273        let response: Option<lsp_types::GotoDefinitionResponse> = client
274            .request(
275                "textDocument/implementation",
276                params,
277                client.request_timeout(),
278            )
279            .await?;
280
281        Ok(LocationsResult {
282            locations: goto_response_to_locations(response, &ctx).await,
283        })
284    }
285
286    /// Handle go-to-type-definition request (`textDocument/typeDefinition`).
287    ///
288    /// Returns the type definition location of the expression at position. Distinct
289    /// from go-to-definition for variable bindings where definition and type differ.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if the LSP request fails, the file cannot be opened,
294    /// or the routed server does not advertise `typeDefinitionProvider` support.
295    pub async fn handle_type_definition(
296        &self,
297        file_path: String,
298        line: u32,
299        character: u32,
300    ) -> Result<LocationsResult> {
301        let (server_id, client, uri) = self
302            .prepare_gated_document(
303                &file_path,
304                ToolKind::TypeDefinition,
305                "typeDefinitionProvider",
306                |caps| {
307                    matches!(
308                        caps.type_definition_provider,
309                        Some(
310                            lsp_types::TypeDefinitionProviderCapability::Simple(true)
311                                | lsp_types::TypeDefinitionProviderCapability::Options(_)
312                        )
313                    )
314                },
315            )
316            .await?;
317        let ctx = self.encoding_ctx(&server_id);
318        let lsp_position = ctx.to_lsp(&uri, line, character).await;
319
320        let params = GotoDefinitionParams {
321            text_document_position_params: TextDocumentPositionParams {
322                text_document: TextDocumentIdentifier { uri },
323                position: lsp_position,
324            },
325            work_done_progress_params: WorkDoneProgressParams::default(),
326            partial_result_params: PartialResultParams::default(),
327        };
328
329        let response: Option<lsp_types::GotoDefinitionResponse> = client
330            .request(
331                "textDocument/typeDefinition",
332                params,
333                client.request_timeout(),
334            )
335            .await?;
336
337        Ok(LocationsResult {
338            locations: goto_response_to_locations(response, &ctx).await,
339        })
340    }
341}
342
343#[cfg(test)]
344#[allow(clippy::unwrap_used, clippy::expect_used)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_extract_hover_contents_string() {
350        let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
351        let contents = lsp_types::HoverContents::Scalar(marked_string);
352        let result = extract_hover_contents(contents);
353        assert_eq!(result, "Test hover");
354    }
355
356    #[test]
357    fn test_extract_hover_contents_language_string() {
358        let marked_string = lsp_types::MarkedString::LanguageString(lsp_types::LanguageString {
359            language: "rust".to_string(),
360            value: "fn main() {}".to_string(),
361        });
362        let contents = lsp_types::HoverContents::Scalar(marked_string);
363        let result = extract_hover_contents(contents);
364        assert_eq!(result, "```rust\nfn main() {}\n```");
365    }
366
367    #[test]
368    fn test_extract_hover_contents_markup() {
369        let markup = lsp_types::MarkupContent {
370            kind: lsp_types::MarkupKind::Markdown,
371            value: "# Documentation".to_string(),
372        };
373        let contents = lsp_types::HoverContents::Markup(markup);
374        let result = extract_hover_contents(contents);
375        assert_eq!(result, "# Documentation");
376    }
377}