ricecoder_lsp/hover/
mod.rs

1//! Hover Information Provider
2//!
3//! This module provides hover information for symbols in code, including type information,
4//! documentation, and definition locations.
5//!
6//! # External LSP Integration
7//!
8//! The hover provider integrates with external LSP servers for semantic hover information:
9//!
10//! 1. **External LSP First**: If an external LSP server is configured for the language,
11//!    it provides semantic hover information (type information, documentation, etc.)
12//! 2. **Fallback**: If the external LSP is unavailable, the system falls back to
13//!    internal hover provider
14//!
15//! # Fallback Behavior
16//!
17//! When external LSP is unavailable, the internal hover provider provides:
18//!
19//! - **Symbol Information**: Basic symbol name and kind
20//! - **Documentation**: Documentation from code comments
21//! - **Definition Location**: File and line number where symbol is defined
22//! - **Reference Count**: Number of references to the symbol
23//!
24//! However, the internal provider lacks:
25//!
26//! - **Type Information**: Cannot infer or display type information
27//! - **Semantic Documentation**: Cannot extract semantic documentation from LSP
28//! - **Project Context**: Cannot resolve symbols across project files
29//! - **Markdown Rendering**: Limited markdown support compared to LSP
30
31pub mod symbol_resolver;
32
33pub use symbol_resolver::{SymbolResolver, SymbolScope};
34
35use crate::types::{HoverInfo, MarkupContent, Position, Symbol};
36use std::collections::HashMap;
37
38/// Hover information provider
39pub struct HoverProvider {
40    /// Symbol index for fast lookup
41    symbol_index: HashMap<String, Symbol>,
42}
43
44impl HoverProvider {
45    /// Create a new hover provider
46    pub fn new() -> Self {
47        Self {
48            symbol_index: HashMap::new(),
49        }
50    }
51
52    /// Index symbols for fast lookup
53    pub fn index_symbols(&mut self, symbols: Vec<Symbol>) {
54        self.symbol_index.clear();
55        for symbol in symbols {
56            self.symbol_index.insert(symbol.name.clone(), symbol);
57        }
58    }
59
60    /// Get hover information at a specific position
61    pub fn get_hover_info(&self, code: &str, position: Position) -> Option<HoverInfo> {
62        // Find the symbol at the given position
63        let symbol = self.find_symbol_at_position(code, position)?;
64
65        // Build hover content
66        let mut content = String::new();
67
68        // Add type information
69        content.push_str(&format!("**{}** `{:?}`\n\n", symbol.name, symbol.kind));
70
71        // Add documentation if available
72        if let Some(doc) = &symbol.documentation {
73            content.push_str(doc);
74            content.push_str("\n\n");
75        }
76
77        // Add definition location
78        if let Some(def) = &symbol.definition {
79            content.push_str(&format!(
80                "**Defined at**: `{}`:{}-{}\n",
81                def.uri, def.range.start.line, def.range.start.character
82            ));
83        }
84
85        // Add usage count
86        let usage_count = symbol.references.len();
87        content.push_str(&format!("**References**: {}", usage_count));
88
89        let hover_info = HoverInfo::new(MarkupContent::markdown(content)).with_range(symbol.range);
90
91        Some(hover_info)
92    }
93
94    /// Find symbol at a specific position in code
95    fn find_symbol_at_position(&self, code: &str, position: Position) -> Option<Symbol> {
96        // Convert position to byte offset
97        let target_offset = self.position_to_offset(code, position)?;
98
99        // Find symbol that contains this position
100        for symbol in self.symbol_index.values() {
101            let start_offset = self.position_to_offset(code, symbol.range.start)?;
102            let end_offset = self.position_to_offset(code, symbol.range.end)?;
103
104            if target_offset >= start_offset && target_offset <= end_offset {
105                return Some(symbol.clone());
106            }
107        }
108
109        None
110    }
111
112    /// Convert position to byte offset in code
113    fn position_to_offset(&self, code: &str, position: Position) -> Option<usize> {
114        let mut offset = 0;
115        let mut current_line = 0;
116        let mut current_char = 0;
117
118        for ch in code.chars() {
119            if current_line == position.line && current_char == position.character {
120                return Some(offset);
121            }
122
123            if ch == '\n' {
124                current_line += 1;
125                current_char = 0;
126            } else {
127                current_char += 1;
128            }
129
130            offset += ch.len_utf8();
131        }
132
133        // Check if we're at the end
134        if current_line == position.line && current_char == position.character {
135            return Some(offset);
136        }
137
138        None
139    }
140
141    /// Get type information for a symbol
142    pub fn get_type_info(&self, symbol_name: &str) -> Option<String> {
143        self.symbol_index
144            .get(symbol_name)
145            .map(|symbol| format!("{:?}", symbol.kind))
146    }
147
148    /// Get definition location for a symbol
149    pub fn get_definition_location(&self, symbol_name: &str) -> Option<(String, u32, u32)> {
150        self.symbol_index.get(symbol_name).and_then(|symbol| {
151            symbol.definition.as_ref().map(|def| {
152                (
153                    def.uri.clone(),
154                    def.range.start.line,
155                    def.range.start.character,
156                )
157            })
158        })
159    }
160
161    /// Get documentation for a symbol
162    pub fn get_documentation(&self, symbol_name: &str) -> Option<String> {
163        self.symbol_index
164            .get(symbol_name)
165            .and_then(|symbol| symbol.documentation.clone())
166    }
167
168    /// Get usage count for a symbol
169    pub fn get_usage_count(&self, symbol_name: &str) -> usize {
170        self.symbol_index
171            .get(symbol_name)
172            .map(|symbol| symbol.references.len())
173            .unwrap_or(0)
174    }
175}
176
177impl Default for HoverProvider {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::types::{Definition, Range, SymbolKind};
187
188    #[test]
189    fn test_hover_provider_creation() {
190        let provider = HoverProvider::new();
191        assert_eq!(provider.symbol_index.len(), 0);
192    }
193
194    #[test]
195    fn test_index_symbols() {
196        let mut provider = HoverProvider::new();
197        let symbol = Symbol {
198            name: "test_fn".to_string(),
199            kind: SymbolKind::Function,
200            range: Range::new(Position::new(0, 0), Position::new(0, 7)),
201            definition: None,
202            references: vec![],
203            documentation: None,
204        };
205
206        provider.index_symbols(vec![symbol.clone()]);
207        assert_eq!(provider.symbol_index.len(), 1);
208        assert!(provider.symbol_index.contains_key("test_fn"));
209    }
210
211    #[test]
212    fn test_get_type_info() {
213        let mut provider = HoverProvider::new();
214        let symbol = Symbol {
215            name: "my_var".to_string(),
216            kind: SymbolKind::Variable,
217            range: Range::new(Position::new(0, 0), Position::new(0, 6)),
218            definition: None,
219            references: vec![],
220            documentation: None,
221        };
222
223        provider.index_symbols(vec![symbol]);
224        let type_info = provider.get_type_info("my_var");
225        assert!(type_info.is_some());
226    }
227
228    #[test]
229    fn test_get_definition_location() {
230        let mut provider = HoverProvider::new();
231        let definition = Definition {
232            uri: "file://test.rs".to_string(),
233            range: Range::new(Position::new(5, 0), Position::new(5, 10)),
234        };
235
236        let symbol = Symbol {
237            name: "my_fn".to_string(),
238            kind: SymbolKind::Function,
239            range: Range::new(Position::new(0, 0), Position::new(0, 5)),
240            definition: Some(definition),
241            references: vec![],
242            documentation: None,
243        };
244
245        provider.index_symbols(vec![symbol]);
246        let location = provider.get_definition_location("my_fn");
247        assert!(location.is_some());
248        let (uri, line, char) = location.unwrap();
249        assert_eq!(uri, "file://test.rs");
250        assert_eq!(line, 5);
251        assert_eq!(char, 0);
252    }
253
254    #[test]
255    fn test_get_documentation() {
256        let mut provider = HoverProvider::new();
257        let symbol = Symbol {
258            name: "documented_fn".to_string(),
259            kind: SymbolKind::Function,
260            range: Range::new(Position::new(0, 0), Position::new(0, 13)),
261            definition: None,
262            references: vec![],
263            documentation: Some("This is a test function".to_string()),
264        };
265
266        provider.index_symbols(vec![symbol]);
267        let doc = provider.get_documentation("documented_fn");
268        assert_eq!(doc, Some("This is a test function".to_string()));
269    }
270
271    #[test]
272    fn test_get_usage_count() {
273        let mut provider = HoverProvider::new();
274        let symbol = Symbol {
275            name: "used_fn".to_string(),
276            kind: SymbolKind::Function,
277            range: Range::new(Position::new(0, 0), Position::new(0, 7)),
278            definition: None,
279            references: vec![
280                crate::types::Reference {
281                    uri: "file://test.rs".to_string(),
282                    range: Range::new(Position::new(10, 0), Position::new(10, 7)),
283                },
284                crate::types::Reference {
285                    uri: "file://test.rs".to_string(),
286                    range: Range::new(Position::new(20, 0), Position::new(20, 7)),
287                },
288            ],
289            documentation: None,
290        };
291
292        provider.index_symbols(vec![symbol]);
293        let count = provider.get_usage_count("used_fn");
294        assert_eq!(count, 2);
295    }
296
297    #[test]
298    fn test_position_to_offset() {
299        let provider = HoverProvider::new();
300        let code = "fn main() {\n    println!(\"hello\");\n}";
301
302        // Test first position
303        let offset = provider.position_to_offset(code, Position::new(0, 0));
304        assert_eq!(offset, Some(0));
305
306        // Test position after newline (line 1, char 0)
307        let offset = provider.position_to_offset(code, Position::new(1, 0));
308        assert_eq!(offset, Some(12)); // "fn main() {\n" = 11 bytes + 1 for newline
309
310        // Test position in middle of line
311        let offset = provider.position_to_offset(code, Position::new(0, 2));
312        assert_eq!(offset, Some(2)); // "fn"
313    }
314
315    #[test]
316    fn test_find_symbol_at_position() {
317        let mut provider = HoverProvider::new();
318        let symbol = Symbol {
319            name: "test_fn".to_string(),
320            kind: SymbolKind::Function,
321            range: Range::new(Position::new(0, 0), Position::new(0, 7)),
322            definition: None,
323            references: vec![],
324            documentation: None,
325        };
326
327        provider.index_symbols(vec![symbol.clone()]);
328
329        let code = "test_fn";
330        let found = provider.find_symbol_at_position(code, Position::new(0, 3));
331        assert!(found.is_some());
332        assert_eq!(found.unwrap().name, "test_fn");
333    }
334
335    #[test]
336    fn test_get_hover_info() {
337        let mut provider = HoverProvider::new();
338        let symbol = Symbol {
339            name: "my_function".to_string(),
340            kind: SymbolKind::Function,
341            range: Range::new(Position::new(0, 0), Position::new(0, 11)),
342            definition: Some(Definition {
343                uri: "file://test.rs".to_string(),
344                range: Range::new(Position::new(5, 0), Position::new(5, 11)),
345            }),
346            references: vec![],
347            documentation: Some("A test function".to_string()),
348        };
349
350        provider.index_symbols(vec![symbol]);
351
352        let code = "my_function";
353        let hover = provider.get_hover_info(code, Position::new(0, 5));
354        assert!(hover.is_some());
355
356        let hover_info = hover.unwrap();
357        assert!(hover_info.contents.value.contains("my_function"));
358        assert!(hover_info.contents.value.contains("A test function"));
359        assert!(hover_info.range.is_some());
360    }
361}