ricecoder_completion/
ghost_text.rs

1/// Ghost text functionality for inline completion suggestions
2///
3/// Ghost text displays the top completion suggestion in a lighter color,
4/// allowing users to see what will be inserted before accepting it.
5use crate::types::{CompletionItem, GhostText, Position, Range};
6
7/// Trait for generating ghost text from completions
8pub trait GhostTextGenerator: Send + Sync {
9    /// Generate ghost text from a completion item
10    fn generate_ghost_text(&self, completion: &CompletionItem, position: Position) -> GhostText;
11
12    /// Generate ghost text for multi-line completions
13    fn generate_multiline_ghost_text(
14        &self,
15        completion: &CompletionItem,
16        position: Position,
17    ) -> GhostText;
18}
19
20/// Basic ghost text generator implementation
21pub struct BasicGhostTextGenerator;
22
23impl BasicGhostTextGenerator {
24    pub fn new() -> Self {
25        Self
26    }
27}
28
29impl Default for BasicGhostTextGenerator {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl GhostTextGenerator for BasicGhostTextGenerator {
36    fn generate_ghost_text(&self, completion: &CompletionItem, position: Position) -> GhostText {
37        // Ghost text starts at the current position and extends to the end of the inserted text
38        let text = completion.insert_text.clone();
39        let end_position = Position::new(position.line, position.character + text.len() as u32);
40
41        GhostText::new(text, Range::new(position, end_position))
42    }
43
44    fn generate_multiline_ghost_text(
45        &self,
46        completion: &CompletionItem,
47        position: Position,
48    ) -> GhostText {
49        let text = completion.insert_text.clone();
50        let lines: Vec<&str> = text.lines().collect();
51
52        let end_position = if lines.len() > 1 {
53            // For multi-line text, end position is on the last line
54            let last_line = lines[lines.len() - 1];
55            Position::new(
56                position.line + (lines.len() - 1) as u32,
57                last_line.len() as u32,
58            )
59        } else {
60            // Single line
61            Position::new(position.line, position.character + text.len() as u32)
62        };
63
64        GhostText::new(text, Range::new(position, end_position))
65    }
66}
67
68/// Ghost text styling information
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum GhostTextStyle {
71    /// Lighter color (typical ghost text)
72    #[default]
73    Faded,
74    /// Italicized
75    Italic,
76    /// Dimmed
77    Dimmed,
78    /// Custom styling
79    Custom,
80}
81
82/// Ghost text renderer trait for UI integration
83pub trait GhostTextRenderer: Send + Sync {
84    /// Render ghost text with specified styling
85    fn render(&self, ghost_text: &GhostText, style: GhostTextStyle) -> String;
86
87    /// Get the styled representation of ghost text
88    fn get_styled_text(&self, ghost_text: &GhostText, style: GhostTextStyle) -> String;
89}
90
91/// Basic ghost text renderer
92pub struct BasicGhostTextRenderer;
93
94impl BasicGhostTextRenderer {
95    pub fn new() -> Self {
96        Self
97    }
98}
99
100impl Default for BasicGhostTextRenderer {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl GhostTextRenderer for BasicGhostTextRenderer {
107    fn render(&self, ghost_text: &GhostText, style: GhostTextStyle) -> String {
108        match style {
109            GhostTextStyle::Faded => format!("(faded) {}", ghost_text.text),
110            GhostTextStyle::Italic => format!("(italic) {}", ghost_text.text),
111            GhostTextStyle::Dimmed => format!("(dimmed) {}", ghost_text.text),
112            GhostTextStyle::Custom => ghost_text.text.clone(),
113        }
114    }
115
116    fn get_styled_text(&self, ghost_text: &GhostText, _style: GhostTextStyle) -> String {
117        ghost_text.text.clone()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_ghost_text_generation_single_line() {
127        let generator = BasicGhostTextGenerator::new();
128        let completion = CompletionItem::new(
129            "println".to_string(),
130            crate::types::CompletionItemKind::Function,
131            "println!(\"Hello\")".to_string(),
132        );
133        let position = Position::new(0, 5);
134
135        let ghost_text = generator.generate_ghost_text(&completion, position);
136
137        assert_eq!(ghost_text.text, "println!(\"Hello\")");
138        assert_eq!(ghost_text.range.start, position);
139        assert_eq!(
140            ghost_text.range.end.character,
141            position.character + "println!(\"Hello\")".len() as u32
142        );
143    }
144
145    #[test]
146    fn test_ghost_text_generation_multiline() {
147        let generator = BasicGhostTextGenerator::new();
148        let completion = CompletionItem::new(
149            "fn".to_string(),
150            crate::types::CompletionItemKind::Keyword,
151            "fn main() {\n    \n}".to_string(),
152        );
153        let position = Position::new(0, 0);
154
155        let ghost_text = generator.generate_multiline_ghost_text(&completion, position);
156
157        assert_eq!(ghost_text.text, "fn main() {\n    \n}");
158        assert_eq!(ghost_text.range.start, position);
159        // End position should be on line 2 (0-indexed)
160        assert_eq!(ghost_text.range.end.line, 2);
161    }
162
163    #[test]
164    fn test_ghost_text_renderer_faded() {
165        let renderer = BasicGhostTextRenderer::new();
166        let ghost_text = GhostText::new(
167            "test".to_string(),
168            Range::new(Position::new(0, 0), Position::new(0, 4)),
169        );
170
171        let rendered = renderer.render(&ghost_text, GhostTextStyle::Faded);
172        assert!(rendered.contains("test"));
173    }
174
175    #[test]
176    fn test_ghost_text_renderer_italic() {
177        let renderer = BasicGhostTextRenderer::new();
178        let ghost_text = GhostText::new(
179            "test".to_string(),
180            Range::new(Position::new(0, 0), Position::new(0, 4)),
181        );
182
183        let rendered = renderer.render(&ghost_text, GhostTextStyle::Italic);
184        assert!(rendered.contains("test"));
185    }
186}