mcpls_core/bridge/translator/
assist.rs1use lsp_types::{
4 CompletionParams, CompletionTriggerKind, InlayHintParams, PartialResultParams,
5 SignatureHelpParams as LspSignatureHelpParams, TextDocumentIdentifier,
6 TextDocumentPositionParams, WorkDoneProgressParams,
7};
8
9use super::Translator;
10use super::dto::{
11 Completion, CompletionsResult, InlayHintEntry, InlayHintsResult, Position, 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 position: Position,
62 trigger: Option<String>,
63 ) -> Result<CompletionsResult> {
64 let Position { line, character } = position;
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::TriggerCharacter,
82 trigger_character: Some(trigger_char),
83 });
84
85 let params = CompletionParams {
86 text_document_position_params: 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 = client
96 .request_typed::<lsp_types::CompletionRequest>(params, client.completion_timeout())
97 .await?;
98
99 let items = match response {
100 Some(lsp_types::CompletionResponse::CompletionItemList(items)) => items,
101 Some(lsp_types::CompletionResponse::CompletionList(list)) => list.items,
102 None => vec![],
103 };
104
105 let result = CompletionsResult {
106 items: items
107 .into_iter()
108 .map(|item| Completion {
109 label: item.label,
110 kind: item.kind.map(|k| format!("{k:?}")),
111 detail: item.detail,
112 documentation: item.documentation.map(|doc| match doc {
113 lsp_types::Documentation::String(s) => s,
114 lsp_types::Documentation::MarkupContent(m) => m.value,
115 }),
116 })
117 .collect(),
118 };
119
120 Ok(result)
121 }
122
123 pub async fn handle_signature_help(
133 &self,
134 file_path: String,
135 position: Position,
136 ) -> Result<SignatureHelpResult> {
137 let Position { line, character } = position;
138 let (server_id, client, uri) = self
139 .prepare_gated_document(
140 &file_path,
141 ToolKind::SignatureHelp,
142 "signatureHelpProvider",
143 |caps| caps.signature_help_provider.is_some(),
144 )
145 .await?;
146 let lsp_position = self
147 .encoding_ctx(&server_id)
148 .to_lsp(&uri, line, character)
149 .await;
150
151 let params = LspSignatureHelpParams {
152 text_document_position_params: TextDocumentPositionParams {
153 text_document: TextDocumentIdentifier { uri },
154 position: lsp_position,
155 },
156 work_done_progress_params: WorkDoneProgressParams::default(),
157 context: None,
158 };
159
160 let response = client
161 .request_typed::<lsp_types::SignatureHelpRequest>(params, client.request_timeout())
162 .await?;
163
164 let result = match response {
165 Some(sig_help) => SignatureHelpResult {
166 signatures: sig_help
167 .signatures
168 .into_iter()
169 .map(|sig| SignatureInfo {
170 label: sig.label,
171 documentation: sig.documentation.map(extract_documentation),
172 parameters: sig
173 .parameters
174 .unwrap_or_default()
175 .into_iter()
176 .map(|p| SignatureParameter {
177 label: match p.label {
178 lsp_types::ParameterInformationLabel::String(s) => s,
179 lsp_types::ParameterInformationLabel::Tuple((start, end)) => {
180 format!("[{start},{end}]")
181 }
182 },
183 documentation: p.documentation.map(extract_documentation),
184 })
185 .collect(),
186 })
187 .collect(),
188 active_signature: sig_help.active_signature,
189 active_parameter: sig_help.active_parameter.and_then(|ap| match ap {
190 lsp_types::ActiveParameter::Int(n) => Some(n),
191 lsp_types::ActiveParameter::Null => None,
192 }),
193 },
194 None => SignatureHelpResult {
195 signatures: vec![],
196 active_signature: None,
197 active_parameter: None,
198 },
199 };
200
201 Ok(result)
202 }
203
204 pub async fn handle_inlay_hints(
214 &self,
215 file_path: String,
216 start: Position,
217 end: Position,
218 ) -> Result<InlayHintsResult> {
219 let (server_id, client, uri) = self
220 .prepare_gated_document(
221 &file_path,
222 ToolKind::InlayHints,
223 "inlayHintProvider",
224 |caps| {
225 matches!(
226 caps.inlay_hint_provider,
227 Some(
228 lsp_types::InlayHintProvider::Bool(true)
229 | lsp_types::InlayHintProvider::InlayHintOptions(_)
230 | lsp_types::InlayHintProvider::InlayHintRegistrationOptions(_)
231 )
232 )
233 },
234 )
235 .await?;
236 let ctx = self.encoding_ctx(&server_id);
237 let response_uri = uri.clone();
238
239 let lsp_start = ctx.to_lsp(&uri, start.line, start.character).await;
240 let lsp_end = ctx.to_lsp(&uri, end.line, end.character).await;
241
242 let params = InlayHintParams {
243 text_document: TextDocumentIdentifier { uri },
244 range: lsp_types::Range {
245 start: lsp_start,
246 end: lsp_end,
247 },
248 work_done_progress_params: WorkDoneProgressParams::default(),
249 };
250
251 let response = client
252 .request_typed::<lsp_types::InlayHintRequest>(params, client.request_timeout())
253 .await?;
254
255 let mut hints = Vec::new();
256 for hint in response.unwrap_or_default() {
257 let position = ctx.to_mcp(&response_uri, hint.position).await;
258 let label = match hint.label {
259 lsp_types::Label::String(s) => s,
260 lsp_types::Label::InlayHintLabelPartList(parts) => parts
261 .into_iter()
262 .map(|p| p.value)
263 .collect::<Vec<_>>()
264 .concat(),
265 };
266 let tooltip = hint.tooltip.map(|t| match t {
267 lsp_types::Tooltip::String(s) => s,
268 lsp_types::Tooltip::MarkupContent(m) => m.value,
269 });
270 hints.push(InlayHintEntry {
271 position,
272 label,
273 kind: hint.kind.and_then(|k| {
274 serde_json::to_value(k)
275 .ok()
276 .and_then(|v| v.as_i64())
277 .and_then(|n| u8::try_from(n).ok())
278 }),
279 padding_left: hint.padding_left,
280 padding_right: hint.padding_right,
281 tooltip,
282 });
283 }
284
285 Ok(InlayHintsResult { hints })
286 }
287}
288
289#[cfg(test)]
290#[allow(clippy::unwrap_used, clippy::expect_used)]
291mod tests {
292 use super::*;
293
294 #[test]
297 fn test_validate_completions_params_rejects_oversized_trigger() {
298 let trigger = "a".repeat(MAX_TRIGGER_CHARACTER_BYTES + 1);
299 let result = validate_completions_params(Some(&trigger));
300 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
301 }
302
303 #[test]
304 fn test_validate_completions_params_accepts_typical_trigger_char() {
305 assert!(validate_completions_params(Some(".")).is_ok());
306 }
307
308 #[test]
309 fn test_validate_completions_params_accepts_none() {
310 assert!(validate_completions_params(None).is_ok());
311 }
312}