1use crate::client::LspConnection;
6use crate::error::Result;
7use crate::mapping::{CompletionMapper, DiagnosticsMapper, HoverMapper};
8use crate::types::{CompletionMappingRules, HoverMappingRules, MergeConfig};
9use ricecoder_completion::types::{CompletionContext, CompletionItem};
10use ricecoder_lsp::types::{Diagnostic, Position, Range};
11use serde_json::{json, Value};
12use std::time::Duration;
13
14pub struct SemanticFeatures {
16 connection: std::sync::Arc<LspConnection>,
18 completion_mapper: CompletionMapper,
20 #[allow(dead_code)]
22 diagnostics_mapper: DiagnosticsMapper,
23 hover_mapper: HoverMapper,
25 #[allow(dead_code)]
27 merge_config: MergeConfig,
28 timeout: Duration,
30}
31
32impl SemanticFeatures {
33 pub fn new(
35 connection: std::sync::Arc<LspConnection>,
36 completion_mapper: CompletionMapper,
37 diagnostics_mapper: DiagnosticsMapper,
38 hover_mapper: HoverMapper,
39 merge_config: MergeConfig,
40 timeout: Duration,
41 ) -> Self {
42 Self {
43 connection,
44 completion_mapper,
45 diagnostics_mapper,
46 hover_mapper,
47 merge_config,
48 timeout,
49 }
50 }
51
52 pub async fn forward_completion(
64 &self,
65 uri: &str,
66 position: Position,
67 _context: &CompletionContext,
68 ) -> Result<Option<Vec<CompletionItem>>> {
69 let params = json!({
71 "textDocument": {
72 "uri": uri
73 },
74 "position": {
75 "line": position.line,
76 "character": position.character
77 }
78 });
79
80 let (_request, mut rx) = self
82 .connection
83 .create_tracked_request("textDocument/completion", Some(params), self.timeout)
84 .await?;
85
86 match tokio::time::timeout(self.timeout, &mut rx).await {
88 Ok(Ok(result)) => {
89 match result {
91 Ok(response) => {
92 let rules = CompletionMappingRules {
95 items_path: "$.result.items".to_string(),
96 field_mappings: Default::default(),
97 transform: None,
98 };
99
100 let mapped_items = self.completion_mapper.map(&response, &rules)?;
101
102 let items = mapped_items
104 .into_iter()
105 .filter_map(|item| {
106 let label = item.get("label")?.as_str()?.to_string();
107 let insert_text = item.get("insertText")
108 .and_then(|v| v.as_str())
109 .unwrap_or(&label)
110 .to_string();
111
112 Some(CompletionItem::new(
113 label,
114 ricecoder_completion::types::CompletionItemKind::Variable,
115 insert_text,
116 ))
117 })
118 .collect();
119
120 Ok(Some(items))
121 }
122 Err(e) => {
123 tracing::warn!("LSP completion request failed: {}", e);
125 Ok(None)
126 }
127 }
128 }
129 Ok(Err(_)) => {
130 Ok(None)
132 }
133 Err(_) => {
134 tracing::warn!("LSP completion request timed out");
136 Ok(None)
137 }
138 }
139 }
140
141 pub async fn forward_diagnostics(&self, _uri: &str) -> Result<Option<Vec<Diagnostic>>> {
151 Ok(None)
158 }
159
160 pub async fn forward_hover(&self, uri: &str, position: Position) -> Result<Option<String>> {
171 let params = json!({
173 "textDocument": {
174 "uri": uri
175 },
176 "position": {
177 "line": position.line,
178 "character": position.character
179 }
180 });
181
182 let (_request, mut rx) = self
184 .connection
185 .create_tracked_request("textDocument/hover", Some(params), self.timeout)
186 .await?;
187
188 match tokio::time::timeout(self.timeout, &mut rx).await {
190 Ok(Ok(result)) => {
191 match result {
193 Ok(response) => {
194 let rules = HoverMappingRules {
196 content_path: "$.result.contents".to_string(),
197 field_mappings: Default::default(),
198 transform: None,
199 };
200
201 let hover_value = self.hover_mapper.map(&response, &rules)?;
202
203 let hover_info = if let Some(s) = hover_value.as_str() {
205 s.to_string()
206 } else {
207 hover_value.to_string()
208 };
209
210 Ok(Some(hover_info))
211 }
212 Err(e) => {
213 tracing::warn!("LSP hover request failed: {}", e);
215 Ok(None)
216 }
217 }
218 }
219 Ok(Err(_)) => {
220 Ok(None)
222 }
223 Err(_) => {
224 tracing::warn!("LSP hover request timed out");
226 Ok(None)
227 }
228 }
229 }
230
231 pub async fn forward_definition(
242 &self,
243 uri: &str,
244 position: Position,
245 ) -> Result<Option<Vec<(String, Range)>>> {
246 let params = json!({
248 "textDocument": {
249 "uri": uri
250 },
251 "position": {
252 "line": position.line,
253 "character": position.character
254 }
255 });
256
257 let (_request, mut rx) = self
259 .connection
260 .create_tracked_request("textDocument/definition", Some(params), self.timeout)
261 .await?;
262
263 match tokio::time::timeout(self.timeout, &mut rx).await {
265 Ok(Ok(result)) => {
266 match result {
268 Ok(response) => {
269 let locations = parse_locations(&response)?;
271 Ok(Some(locations))
272 }
273 Err(e) => {
274 tracing::warn!("LSP definition request failed: {}", e);
276 Ok(None)
277 }
278 }
279 }
280 Ok(Err(_)) => {
281 Ok(None)
283 }
284 Err(_) => {
285 tracing::warn!("LSP definition request timed out");
287 Ok(None)
288 }
289 }
290 }
291
292 pub async fn forward_references(
303 &self,
304 uri: &str,
305 position: Position,
306 ) -> Result<Option<Vec<(String, Range)>>> {
307 let params = json!({
309 "textDocument": {
310 "uri": uri
311 },
312 "position": {
313 "line": position.line,
314 "character": position.character
315 },
316 "context": {
317 "includeDeclaration": true
318 }
319 });
320
321 let (_request, mut rx) = self
323 .connection
324 .create_tracked_request("textDocument/references", Some(params), self.timeout)
325 .await?;
326
327 match tokio::time::timeout(self.timeout, &mut rx).await {
329 Ok(Ok(result)) => {
330 match result {
332 Ok(response) => {
333 let locations = parse_locations(&response)?;
335 Ok(Some(locations))
336 }
337 Err(e) => {
338 tracing::warn!("LSP references request failed: {}", e);
340 Ok(None)
341 }
342 }
343 }
344 Ok(Err(_)) => {
345 Ok(None)
347 }
348 Err(_) => {
349 tracing::warn!("LSP references request timed out");
351 Ok(None)
352 }
353 }
354 }
355}
356
357fn parse_locations(response: &Value) -> Result<Vec<(String, Range)>> {
359 let mut locations = Vec::new();
360
361 let items = if response.is_array() {
363 response.as_array().unwrap().clone()
364 } else if response.is_object() {
365 vec![response.clone()]
366 } else {
367 return Ok(locations);
368 };
369
370 for item in items {
371 if let (Some(uri), Some(range)) = (item.get("uri").and_then(|v| v.as_str()), item.get("range")) {
372 if let Some(parsed_range) = parse_range(range) {
373 locations.push((uri.to_string(), parsed_range));
374 }
375 }
376 }
377
378 Ok(locations)
379}
380
381fn parse_range(range: &Value) -> Option<Range> {
383 let start = range.get("start")?;
384 let end = range.get("end")?;
385
386 let start_line = start.get("line")?.as_u64()? as u32;
387 let start_char = start.get("character")?.as_u64()? as u32;
388 let end_line = end.get("line")?.as_u64()? as u32;
389 let end_char = end.get("character")?.as_u64()? as u32;
390
391 Some(Range::new(
392 Position::new(start_line, start_char),
393 Position::new(end_line, end_char),
394 ))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_parse_single_location() {
403 let response = json!({
404 "uri": "file:///test.rs",
405 "range": {
406 "start": {"line": 0, "character": 0},
407 "end": {"line": 0, "character": 5}
408 }
409 });
410
411 let locations = parse_locations(&response).unwrap();
412 assert_eq!(locations.len(), 1);
413 assert_eq!(locations[0].0, "file:///test.rs");
414 assert_eq!(locations[0].1.start.line, 0);
415 assert_eq!(locations[0].1.start.character, 0);
416 assert_eq!(locations[0].1.end.line, 0);
417 assert_eq!(locations[0].1.end.character, 5);
418 }
419
420 #[test]
421 fn test_parse_multiple_locations() {
422 let response = json!([
423 {
424 "uri": "file:///test1.rs",
425 "range": {
426 "start": {"line": 0, "character": 0},
427 "end": {"line": 0, "character": 5}
428 }
429 },
430 {
431 "uri": "file:///test2.rs",
432 "range": {
433 "start": {"line": 1, "character": 10},
434 "end": {"line": 1, "character": 15}
435 }
436 }
437 ]);
438
439 let locations = parse_locations(&response).unwrap();
440 assert_eq!(locations.len(), 2);
441 assert_eq!(locations[0].0, "file:///test1.rs");
442 assert_eq!(locations[1].0, "file:///test2.rs");
443 }
444
445 #[test]
446 fn test_parse_empty_response() {
447 let response = json!([]);
448 let locations = parse_locations(&response).unwrap();
449 assert_eq!(locations.len(), 0);
450 }
451
452 #[test]
453 fn test_parse_invalid_range() {
454 let response = json!({
455 "uri": "file:///test.rs",
456 "range": {
457 "start": {"line": "invalid"},
458 "end": {"line": 0, "character": 5}
459 }
460 });
461
462 let locations = parse_locations(&response).unwrap();
463 assert_eq!(locations.len(), 0);
464 }
465}