mcpls_core/bridge/translator/
assist.rs1use lsp_types::{
4 CompletionParams, CompletionTriggerKind, InlayHintLabel, InlayHintParams, PartialResultParams,
5 SignatureHelpParams as LspSignatureHelpParams, TextDocumentIdentifier,
6 TextDocumentPositionParams, WorkDoneProgressParams,
7};
8
9use super::Translator;
10use super::dto::{
11 Completion, CompletionsResult, InlayHintEntry, InlayHintsResult, SignatureHelpResult,
12 SignatureInfo, SignatureParameter,
13};
14use crate::config::ToolKind;
15use crate::error::{Error, Result};
16
17fn extract_documentation(doc: lsp_types::Documentation) -> String {
20 match doc {
21 lsp_types::Documentation::String(s) => s,
22 lsp_types::Documentation::MarkupContent(m) => m.value,
23 }
24}
25
26pub(super) const MAX_TRIGGER_CHARACTER_BYTES: usize = 8;
36
37fn validate_completions_params(trigger: Option<&str>) -> Result<()> {
39 if let Some(trigger) = trigger
40 && trigger.len() > MAX_TRIGGER_CHARACTER_BYTES
41 {
42 return Err(Error::InvalidToolParams(format!(
43 "trigger too long: {} bytes (max {MAX_TRIGGER_CHARACTER_BYTES})",
44 trigger.len()
45 )));
46 }
47 Ok(())
48}
49
50impl Translator {
51 pub async fn handle_completions(
59 &self,
60 file_path: String,
61 line: u32,
62 character: u32,
63 trigger: Option<String>,
64 ) -> Result<CompletionsResult> {
65 validate_completions_params(trigger.as_deref())?;
66
67 let (server_id, client, uri) = self
68 .prepare_gated_document(
69 &file_path,
70 ToolKind::Completions,
71 "completionProvider",
72 |caps| caps.completion_provider.is_some(),
73 )
74 .await?;
75 let lsp_position = self
76 .encoding_ctx(&server_id)
77 .to_lsp(&uri, line, character)
78 .await;
79
80 let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
81 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
82 trigger_character: Some(trigger_char),
83 });
84
85 let params = CompletionParams {
86 text_document_position: TextDocumentPositionParams {
87 text_document: TextDocumentIdentifier { uri },
88 position: lsp_position,
89 },
90 work_done_progress_params: WorkDoneProgressParams::default(),
91 partial_result_params: PartialResultParams::default(),
92 context,
93 };
94
95 let response: Option<lsp_types::CompletionResponse> = client
96 .request(
97 "textDocument/completion",
98 params,
99 client.completion_timeout(),
100 )
101 .await?;
102
103 let items = match response {
104 Some(lsp_types::CompletionResponse::Array(items)) => items,
105 Some(lsp_types::CompletionResponse::List(list)) => list.items,
106 None => vec![],
107 };
108
109 let result = CompletionsResult {
110 items: items
111 .into_iter()
112 .map(|item| Completion {
113 label: item.label,
114 kind: item.kind.map(|k| format!("{k:?}")),
115 detail: item.detail,
116 documentation: item.documentation.map(|doc| match doc {
117 lsp_types::Documentation::String(s) => s,
118 lsp_types::Documentation::MarkupContent(m) => m.value,
119 }),
120 })
121 .collect(),
122 };
123
124 Ok(result)
125 }
126
127 pub async fn handle_signature_help(
137 &self,
138 file_path: String,
139 line: u32,
140 character: u32,
141 ) -> Result<SignatureHelpResult> {
142 let (server_id, client, uri) = self
143 .prepare_gated_document(
144 &file_path,
145 ToolKind::SignatureHelp,
146 "signatureHelpProvider",
147 |caps| caps.signature_help_provider.is_some(),
148 )
149 .await?;
150 let lsp_position = self
151 .encoding_ctx(&server_id)
152 .to_lsp(&uri, line, character)
153 .await;
154
155 let params = LspSignatureHelpParams {
156 text_document_position_params: TextDocumentPositionParams {
157 text_document: TextDocumentIdentifier { uri },
158 position: lsp_position,
159 },
160 work_done_progress_params: WorkDoneProgressParams::default(),
161 context: None,
162 };
163
164 let response: Option<lsp_types::SignatureHelp> = client
165 .request(
166 "textDocument/signatureHelp",
167 params,
168 client.request_timeout(),
169 )
170 .await?;
171
172 let result = match response {
173 Some(sig_help) => SignatureHelpResult {
174 signatures: sig_help
175 .signatures
176 .into_iter()
177 .map(|sig| SignatureInfo {
178 label: sig.label,
179 documentation: sig.documentation.map(extract_documentation),
180 parameters: sig
181 .parameters
182 .unwrap_or_default()
183 .into_iter()
184 .map(|p| SignatureParameter {
185 label: match p.label {
186 lsp_types::ParameterLabel::Simple(s) => s,
187 lsp_types::ParameterLabel::LabelOffsets([start, end]) => {
188 format!("[{start},{end}]")
189 }
190 },
191 documentation: p.documentation.map(extract_documentation),
192 })
193 .collect(),
194 })
195 .collect(),
196 active_signature: sig_help.active_signature,
197 active_parameter: sig_help.active_parameter,
198 },
199 None => SignatureHelpResult {
200 signatures: vec![],
201 active_signature: None,
202 active_parameter: None,
203 },
204 };
205
206 Ok(result)
207 }
208
209 pub async fn handle_inlay_hints(
219 &self,
220 file_path: String,
221 start_line: u32,
222 start_character: u32,
223 end_line: u32,
224 end_character: u32,
225 ) -> Result<InlayHintsResult> {
226 let (server_id, client, uri) = self
227 .prepare_gated_document(
228 &file_path,
229 ToolKind::InlayHints,
230 "inlayHintProvider",
231 |caps| {
232 matches!(
233 caps.inlay_hint_provider,
234 Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
235 )
236 },
237 )
238 .await?;
239 let ctx = self.encoding_ctx(&server_id);
240 let response_uri = uri.clone();
241
242 let lsp_start = ctx.to_lsp(&uri, start_line, start_character).await;
243 let lsp_end = ctx.to_lsp(&uri, end_line, end_character).await;
244
245 let params = InlayHintParams {
246 text_document: TextDocumentIdentifier { uri },
247 range: lsp_types::Range {
248 start: lsp_start,
249 end: lsp_end,
250 },
251 work_done_progress_params: WorkDoneProgressParams::default(),
252 };
253
254 let response: Option<Vec<lsp_types::InlayHint>> = client
255 .request("textDocument/inlayHint", params, client.request_timeout())
256 .await?;
257
258 let mut hints = Vec::new();
259 for hint in response.unwrap_or_default() {
260 let position = ctx.to_mcp(&response_uri, hint.position).await;
261 let label = match hint.label {
262 InlayHintLabel::String(s) => s,
263 InlayHintLabel::LabelParts(parts) => parts
264 .into_iter()
265 .map(|p| p.value)
266 .collect::<Vec<_>>()
267 .concat(),
268 };
269 let tooltip = hint.tooltip.map(|t| match t {
270 lsp_types::InlayHintTooltip::String(s) => s,
271 lsp_types::InlayHintTooltip::MarkupContent(m) => m.value,
272 });
273 hints.push(InlayHintEntry {
274 position,
275 label,
276 kind: hint.kind.and_then(|k| {
277 serde_json::to_value(k)
278 .ok()
279 .and_then(|v| v.as_i64())
280 .and_then(|n| u8::try_from(n).ok())
281 }),
282 padding_left: hint.padding_left,
283 padding_right: hint.padding_right,
284 tooltip,
285 });
286 }
287
288 Ok(InlayHintsResult { hints })
289 }
290}
291
292#[cfg(test)]
293#[allow(clippy::unwrap_used, clippy::expect_used)]
294mod tests {
295 use super::*;
296
297 #[test]
300 fn test_validate_completions_params_rejects_oversized_trigger() {
301 let trigger = "a".repeat(MAX_TRIGGER_CHARACTER_BYTES + 1);
302 let result = validate_completions_params(Some(&trigger));
303 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
304 }
305
306 #[test]
307 fn test_validate_completions_params_accepts_typical_trigger_char() {
308 assert!(validate_completions_params(Some(".")).is_ok());
309 }
310
311 #[test]
312 fn test_validate_completions_params_accepts_none() {
313 assert!(validate_completions_params(None).is_ok());
314 }
315}