Skip to main content

mq_edit/renderer/
code.rs

1use std::collections::HashMap;
2
3use lsp_types::SemanticTokens;
4use ratatui::{
5    style::{Color, Style},
6    text::Span,
7};
8use syntect::{
9    easy::HighlightLines,
10    highlighting::{Theme, ThemeSet},
11    parsing::{SyntaxDefinition, SyntaxReference, SyntaxSet, SyntaxSetBuilder},
12};
13
14use super::Renderer;
15use crate::document::DocumentBuffer;
16
17/// Semantic token information from LSP
18#[derive(Debug, Clone)]
19pub struct SemanticToken {
20    pub start: usize,    // Character offset in line
21    pub length: usize,   // Token length in characters
22    pub token_type: u32, // Token type from LSP
23    pub modifiers: u32,  // Token modifiers
24}
25
26/// Convert LSP SemanticTokens to line-based tokens
27pub fn decode_semantic_tokens(
28    tokens: &SemanticTokens,
29    _content: &str,
30) -> HashMap<usize, Vec<SemanticToken>> {
31    let mut result: HashMap<usize, Vec<SemanticToken>> = HashMap::new();
32
33    let data = &tokens.data;
34    let mut current_line = 0;
35    let mut current_start = 0;
36
37    for lsp_token in data {
38        let delta_line = lsp_token.delta_line;
39        let delta_start = lsp_token.delta_start;
40        let length = lsp_token.length;
41        let token_type = lsp_token.token_type;
42        let token_modifiers_bitset = lsp_token.token_modifiers_bitset;
43
44        // Update position
45        if delta_line > 0 {
46            current_line += delta_line as usize;
47            current_start = delta_start as usize;
48        } else {
49            current_start += delta_start as usize;
50        }
51
52        // Create our token structure
53        let token = SemanticToken {
54            start: current_start,
55            length: length as usize,
56            token_type,
57            modifiers: token_modifiers_bitset,
58        };
59
60        result.entry(current_line).or_default().push(token);
61    }
62
63    result
64}
65
66/// Code renderer with syntax highlighting
67///
68/// Supports two modes:
69/// 1. Syntect (default) - Static syntax highlighting
70/// 2. LSP Semantic Tokens (optional) - Semantic highlighting from language servers
71pub struct CodeRenderer {
72    /// Default syntect syntax set for static highlighting
73    default_syntax_set: SyntaxSet,
74    /// Custom syntax set with mq language
75    mq_syntax_set: SyntaxSet,
76    /// Theme for syntax highlighting
77    theme: Theme,
78    /// Theme set for loading themes
79    theme_set: ThemeSet,
80    /// LSP semantic tokens cache (line_idx -> tokens)
81    semantic_tokens: HashMap<usize, Vec<SemanticToken>>,
82    /// Whether to use semantic tokens for highlighting
83    use_semantic_tokens: bool,
84}
85
86/// Embedded mq language syntax definition (sublime-syntax format)
87const MQ_SUBLIME_SYNTAX: &str = include_str!("../../mq.sublime-syntax");
88
89impl CodeRenderer {
90    pub fn new() -> Self {
91        Self::with_theme("base16-ocean.dark")
92    }
93
94    /// Create a new code renderer with specified theme
95    pub fn with_theme(theme_name: &str) -> Self {
96        let default_syntax_set = SyntaxSet::load_defaults_newlines();
97        let mq_syntax_set = Self::build_mq_syntax_set();
98        let theme_set = ThemeSet::load_defaults();
99        let theme = theme_set
100            .themes
101            .get(theme_name)
102            .or_else(|| theme_set.themes.get("base16-ocean.dark"))
103            .or_else(|| theme_set.themes.values().next())
104            .cloned()
105            .unwrap_or_default();
106
107        Self {
108            default_syntax_set,
109            mq_syntax_set,
110            theme,
111            theme_set,
112            semantic_tokens: HashMap::new(),
113            use_semantic_tokens: false,
114        }
115    }
116
117    /// Set the theme by name
118    pub fn set_theme(&mut self, theme_name: &str) {
119        if let Some(theme) = self.theme_set.themes.get(theme_name) {
120            self.theme = theme.clone();
121        }
122    }
123
124    /// Get list of available theme names
125    pub fn available_themes() -> Vec<String> {
126        let theme_set = ThemeSet::load_defaults();
127        let mut themes: Vec<String> = theme_set.themes.keys().cloned().collect();
128        themes.sort();
129        themes
130    }
131
132    /// Build syntax set with mq language
133    fn build_mq_syntax_set() -> SyntaxSet {
134        let mut builder = SyntaxSetBuilder::new();
135        builder.add_plain_text_syntax();
136
137        if let Ok(mq_syntax) = SyntaxDefinition::load_from_str(
138            MQ_SUBLIME_SYNTAX,
139            true, // lines_include_newline
140            Some("mq"),
141        ) {
142            builder.add(mq_syntax);
143        }
144
145        builder.build()
146    }
147
148    /// Update semantic tokens from LSP
149    pub fn set_semantic_tokens(&mut self, tokens: HashMap<usize, Vec<SemanticToken>>) {
150        self.semantic_tokens = tokens;
151    }
152
153    /// Clear semantic tokens
154    pub fn clear_semantic_tokens(&mut self) {
155        self.semantic_tokens.clear();
156    }
157
158    /// Set whether to use semantic tokens for highlighting
159    pub fn set_use_semantic_tokens(&mut self, use_semantic_tokens: bool) {
160        self.use_semantic_tokens = use_semantic_tokens;
161    }
162
163    /// Get syntax reference and corresponding syntax set for a language
164    fn get_syntax(&self, language: &str) -> Option<(&SyntaxReference, &SyntaxSet)> {
165        if let Some(syntax) = self.mq_syntax_set.find_syntax_by_token(language) {
166            return Some((syntax, &self.mq_syntax_set));
167        }
168
169        self.default_syntax_set
170            .find_syntax_by_token(language)
171            .map(|s| (s, &self.default_syntax_set))
172    }
173
174    /// Render line with LSP semantic tokens
175    fn render_with_semantic_tokens(
176        &self,
177        content: &str,
178        tokens: &[SemanticToken],
179    ) -> Vec<Span<'_>> {
180        if tokens.is_empty() {
181            return vec![Span::raw(content.to_string())];
182        }
183
184        let mut spans = Vec::new();
185        let mut last_end = 0;
186
187        for token in tokens {
188            // Add unstyled text before token
189            if token.start > last_end {
190                let text = content
191                    .chars()
192                    .skip(last_end)
193                    .take(token.start - last_end)
194                    .collect::<String>();
195                spans.push(Span::raw(text));
196            }
197
198            // Add styled token
199            let text = content
200                .chars()
201                .skip(token.start)
202                .take(token.length)
203                .collect::<String>();
204            let style = self.semantic_token_style(token.token_type, token.modifiers);
205            spans.push(Span::styled(text, style));
206
207            last_end = token.start + token.length;
208        }
209
210        // Add remaining text
211        if last_end < content.chars().count() {
212            let text = content.chars().skip(last_end).collect::<String>();
213            spans.push(Span::raw(text));
214        }
215
216        spans
217    }
218
219    /// Get style for semantic token type
220    fn semantic_token_style(&self, token_type: u32, _modifiers: u32) -> Style {
221        // Map LSP semantic token types to colors
222        // See: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
223        match token_type {
224            0 => Style::default().fg(Color::Cyan),     // namespace
225            1 => Style::default().fg(Color::Yellow),   // type
226            2 => Style::default().fg(Color::Yellow),   // class
227            3 => Style::default().fg(Color::Yellow),   // enum
228            4 => Style::default().fg(Color::Cyan),     // interface
229            5 => Style::default().fg(Color::Yellow),   // struct
230            6 => Style::default().fg(Color::Magenta),  // typeParameter
231            7 => Style::default().fg(Color::White),    // parameter
232            8 => Style::default().fg(Color::White),    // variable
233            9 => Style::default().fg(Color::Cyan),     // property
234            10 => Style::default().fg(Color::Green),   // enumMember
235            11 => Style::default().fg(Color::Blue),    // function
236            12 => Style::default().fg(Color::Blue),    // method
237            13 => Style::default().fg(Color::Magenta), // macro
238            14 => Style::default().fg(Color::Magenta), // keyword
239            15 => Style::default().fg(Color::Gray),    // comment
240            16 => Style::default().fg(Color::Green),   // string
241            17 => Style::default().fg(Color::Green),   // number
242            18 => Style::default().fg(Color::Magenta), // operator
243            _ => Style::default(),
244        }
245    }
246
247    /// Render line with syntect (fallback)
248    fn render_with_syntect(&self, content: &str, language: &str) -> Vec<Span<'_>> {
249        if let Some((syntax, syntax_set)) = self.get_syntax(language) {
250            let mut highlighter = HighlightLines::new(syntax, &self.theme);
251
252            match highlighter.highlight_line(content, syntax_set) {
253                Ok(regions) => regions
254                    .iter()
255                    .map(|(style, text)| {
256                        let fg_color =
257                            Color::Rgb(style.foreground.r, style.foreground.g, style.foreground.b);
258                        Span::styled(text.to_string(), Style::default().fg(fg_color))
259                    })
260                    .collect(),
261                Err(_) => vec![Span::raw(content.to_string())],
262            }
263        } else {
264            // Unknown language, return plain text
265            vec![Span::raw(content.to_string())]
266        }
267    }
268}
269
270impl Default for CodeRenderer {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276impl Renderer for CodeRenderer {
277    fn render_line(
278        &self,
279        buffer: &DocumentBuffer,
280        line_idx: usize,
281        is_current_line: bool,
282    ) -> Vec<Span<'_>> {
283        let content = buffer.line(line_idx).unwrap_or("");
284
285        if is_current_line {
286            return vec![Span::styled(content.to_string(), Style::default())];
287        }
288
289        let language = match buffer.document_type() {
290            crate::document::DocumentType::Code { language } => language.as_str(),
291            _ => return vec![Span::raw(content.to_string())],
292        };
293
294        if self.use_semantic_tokens
295            && let Some(tokens) = self.semantic_tokens.get(&line_idx)
296        {
297            return self.render_with_semantic_tokens(content, tokens);
298        }
299
300        self.render_with_syntect(content, language)
301    }
302
303    fn supports_wysiwyg(&self) -> bool {
304        true // Cursor line = source, other lines = highlighted
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_code_renderer_creation() {
314        let renderer = CodeRenderer::new();
315        assert!(renderer.supports_wysiwyg());
316        assert!(renderer.semantic_tokens.is_empty());
317    }
318
319    #[test]
320    fn test_semantic_tokens_update() {
321        let mut renderer = CodeRenderer::new();
322        let mut tokens = HashMap::new();
323        tokens.insert(
324            0,
325            vec![SemanticToken {
326                start: 0,
327                length: 3,
328                token_type: 14, // keyword
329                modifiers: 0,
330            }],
331        );
332
333        renderer.set_semantic_tokens(tokens);
334        assert_eq!(renderer.semantic_tokens.len(), 1);
335
336        renderer.clear_semantic_tokens();
337        assert!(renderer.semantic_tokens.is_empty());
338    }
339}