ricecoder_external_lsp/mapping/
hover.rs

1//! Hover output mapping
2//!
3//! Maps LSP hover responses to ricecoder HoverInfo models.
4//! Supports custom field mappings via configuration and transformation functions.
5
6use crate::error::Result;
7use crate::types::HoverMappingRules;
8use serde_json::Value;
9
10use super::transformer::OutputTransformer;
11
12/// Maps LSP hover responses to ricecoder models
13#[derive(Debug, Clone)]
14pub struct HoverMapper {
15    transformer: OutputTransformer,
16}
17
18impl HoverMapper {
19    /// Create a new hover mapper
20    pub fn new() -> Self {
21        Self {
22            transformer: OutputTransformer::new(),
23        }
24    }
25
26    /// Create a mapper with custom transformations
27    pub fn with_transformer(transformer: OutputTransformer) -> Self {
28        Self { transformer }
29    }
30
31    /// Map an LSP hover response to ricecoder models
32    ///
33    /// # Arguments
34    ///
35    /// * `response` - The LSP server response (typically from textDocument/hover)
36    /// * `rules` - The mapping rules from configuration
37    ///
38    /// # Returns
39    ///
40    /// The mapped hover information
41    pub fn map(&self, response: &Value, rules: &HoverMappingRules) -> Result<Value> {
42        self.transformer.transform_hover(response, rules)
43    }
44
45    /// Map hover content directly
46    ///
47    /// This is useful when you already have the hover content extracted
48    /// and just need to apply field mappings.
49    pub fn map_content(&self, content: &Value, rules: &HoverMappingRules) -> Result<Value> {
50        // Create a wrapper response with the content
51        let wrapped = serde_json::json!({
52            "result": {
53                "contents": content
54            }
55        });
56
57        // Use default rules that expect this structure
58        let default_rules = HoverMappingRules {
59            content_path: "$.result.contents".to_string(),
60            field_mappings: rules.field_mappings.clone(),
61            transform: rules.transform.clone(),
62        };
63
64        self.transformer.transform_hover(&wrapped, &default_rules)
65    }
66}
67
68impl Default for HoverMapper {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use std::collections::HashMap;
78
79    #[test]
80    fn test_map_hover_response() {
81        let mapper = HoverMapper::new();
82        let response = serde_json::json!({
83            "result": {
84                "contents": {
85                    "language": "rust",
86                    "value": "fn foo() -> i32"
87                }
88            }
89        });
90
91        let mut field_mappings = HashMap::new();
92        field_mappings.insert("language".to_string(), "$.language".to_string());
93        field_mappings.insert("value".to_string(), "$.value".to_string());
94
95        let rules = HoverMappingRules {
96            content_path: "$.result.contents".to_string(),
97            field_mappings,
98            transform: None,
99        };
100
101        let result = mapper.map(&response, &rules).unwrap();
102        assert_eq!(result["language"], "rust");
103        assert_eq!(result["value"], "fn foo() -> i32");
104    }
105
106    #[test]
107    fn test_map_hover_with_custom_structure() {
108        let mapper = HoverMapper::new();
109        let response = serde_json::json!({
110            "hover_info": {
111                "doc": "A function that returns an integer",
112                "signature": "fn foo() -> i32"
113            }
114        });
115
116        let mut field_mappings = HashMap::new();
117        field_mappings.insert("documentation".to_string(), "$.doc".to_string());
118        field_mappings.insert("signature".to_string(), "$.signature".to_string());
119
120        let rules = HoverMappingRules {
121            content_path: "$.hover_info".to_string(),
122            field_mappings,
123            transform: None,
124        };
125
126        let result = mapper.map(&response, &rules).unwrap();
127        assert_eq!(result["documentation"], "A function that returns an integer");
128        assert_eq!(result["signature"], "fn foo() -> i32");
129    }
130
131    #[test]
132    fn test_map_hover_content() {
133        let mapper = HoverMapper::new();
134        let content = serde_json::json!({
135            "language": "python",
136            "value": "def bar(): pass"
137        });
138
139        let mut field_mappings = HashMap::new();
140        field_mappings.insert("language".to_string(), "$.language".to_string());
141        field_mappings.insert("value".to_string(), "$.value".to_string());
142
143        let rules = HoverMappingRules {
144            content_path: "$.result.contents".to_string(),
145            field_mappings,
146            transform: None,
147        };
148
149        let result = mapper.map_content(&content, &rules).unwrap();
150        assert_eq!(result["language"], "python");
151        assert_eq!(result["value"], "def bar(): pass");
152    }
153
154    #[test]
155    fn test_map_hover_with_markdown() {
156        let mapper = HoverMapper::new();
157        let response = serde_json::json!({
158            "result": {
159                "contents": {
160                    "kind": "markdown",
161                    "value": "# Function\n\nThis is a function"
162                }
163            }
164        });
165
166        let mut field_mappings = HashMap::new();
167        field_mappings.insert("kind".to_string(), "$.kind".to_string());
168        field_mappings.insert("value".to_string(), "$.value".to_string());
169
170        let rules = HoverMappingRules {
171            content_path: "$.result.contents".to_string(),
172            field_mappings,
173            transform: None,
174        };
175
176        let result = mapper.map(&response, &rules).unwrap();
177        assert_eq!(result["kind"], "markdown");
178        assert!(result["value"].as_str().unwrap().contains("Function"));
179    }
180
181    #[test]
182    fn test_map_hover_missing_field() {
183        let mapper = HoverMapper::new();
184        let response = serde_json::json!({
185            "result": {
186                "contents": {
187                    "value": "fn foo() -> i32"
188                }
189            }
190        });
191
192        let mut field_mappings = HashMap::new();
193        field_mappings.insert("language".to_string(), "$.language".to_string()); // Missing
194        field_mappings.insert("value".to_string(), "$.value".to_string());
195
196        let rules = HoverMappingRules {
197            content_path: "$.result.contents".to_string(),
198            field_mappings,
199            transform: None,
200        };
201
202        let result = mapper.map(&response, &rules).unwrap();
203        assert_eq!(result["value"], "fn foo() -> i32");
204        // Missing field should not be in result
205        assert!(!result.get("language").is_some() || result["language"].is_null());
206    }
207}