Skip to main content

gpui_component/highlighter/
wasm_stub.rs

1//! WASM stub implementation for highlighter module.
2//! Provides empty/no-op implementations since tree-sitter is not available in WASM.
3//!
4//! Note: diagnostics.rs is available in WASM, only syntax highlighting requires stubs.
5
6use gpui::{HighlightStyle, SharedString};
7use std::ops::Range;
8use std::time::Duration;
9
10// Syntax highlighter stub
11pub struct SyntaxHighlighter;
12
13impl SyntaxHighlighter {
14    pub fn new(_language: impl AsRef<str>) -> Self {
15        Self
16    }
17
18    pub fn highlight(&self, _text: &ropey::Rope) -> Vec<(Range<usize>, HighlightStyle)> {
19        Vec::new()
20    }
21
22    pub fn styles(
23        &self,
24        range: &Range<usize>,
25        _theme: &HighlightTheme,
26    ) -> Vec<(Range<usize>, HighlightStyle)> {
27        // If the matched styles is empty, return a default range.
28        vec![(range.clone(), HighlightStyle::default())]
29    }
30
31    pub fn update(
32        &mut self,
33        _edit: Option<crate::input::InputEdit>,
34        _text: &ropey::Rope,
35        _timeout: Option<Duration>,
36    ) -> bool {
37        // No-op in WASM
38        true
39    }
40
41    pub fn edit_tree(&mut self, _edit: Option<crate::input::InputEdit>, _text: &ropey::Rope) {
42        // No-op in WASM
43    }
44
45    pub fn language(&self) -> &SharedString {
46        static EMPTY: SharedString = SharedString::new_static("");
47        &EMPTY
48    }
49
50    pub fn text(&self) -> &ropey::Rope {
51        static EMPTY_ROPE: LazyLock<ropey::Rope> = LazyLock::new(ropey::Rope::new);
52        &EMPTY_ROPE
53    }
54
55    pub fn tree(&self) -> Option<&crate::input::Tree> {
56        None
57    }
58}
59
60// Language enum stub
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum Language {
63    Unknown,
64}
65
66impl Language {
67    pub fn from_str(_name: &str) -> Self {
68        Language::Unknown
69    }
70
71    pub fn name(&self) -> &'static str {
72        "unknown"
73    }
74
75    pub fn config(&self) -> GrammarConfig {
76        GrammarConfig {
77            name: "unknown".into(),
78        }
79    }
80
81    pub fn all() -> impl Iterator<Item = Self> {
82        std::iter::once(Language::Unknown)
83    }
84}
85
86// Language config stub (without tree_sitter::Language)
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct LanguageConfig {
89    pub name: SharedString,
90}
91
92/// Explicit name for grammar resources; editing rules use `input::language_config::LanguageConfig`.
93pub type GrammarConfig = LanguageConfig;
94
95impl LanguageConfig {
96    pub fn has_grammar(&self) -> bool {
97        false
98    }
99}
100
101// Re-export theme types from registry module (which will be conditionally compiled)
102// For WASM, we create minimal stubs here
103use schemars::JsonSchema;
104use serde::{Deserialize, Serialize};
105use serde_repr::{Deserialize_repr, Serialize_repr};
106use std::{
107    collections::HashMap,
108    sync::{LazyLock, Mutex},
109};
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum FontStyle {
114    Normal,
115    Italic,
116    Underline,
117}
118
119#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, JsonSchema, Serialize_repr, Deserialize_repr)]
120#[repr(u16)]
121pub enum FontWeightContent {
122    Thin = 100,
123    ExtraLight = 200,
124    Light = 300,
125    Normal = 400,
126    Medium = 500,
127    Semibold = 600,
128    Bold = 700,
129    ExtraBold = 800,
130    Black = 900,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
134pub struct ThemeStyle {
135    pub color: Option<gpui::Hsla>,
136    pub font_style: Option<FontStyle>,
137    pub font_weight: Option<FontWeightContent>,
138}
139
140impl From<ThemeStyle> for HighlightStyle {
141    fn from(style: ThemeStyle) -> Self {
142        HighlightStyle {
143            color: style.color,
144            font_weight: style.font_weight.map(|w| match w {
145                FontWeightContent::Thin => gpui::FontWeight::THIN,
146                FontWeightContent::ExtraLight => gpui::FontWeight::EXTRA_LIGHT,
147                FontWeightContent::Light => gpui::FontWeight::LIGHT,
148                FontWeightContent::Normal => gpui::FontWeight::NORMAL,
149                FontWeightContent::Medium => gpui::FontWeight::MEDIUM,
150                FontWeightContent::Semibold => gpui::FontWeight::SEMIBOLD,
151                FontWeightContent::Bold => gpui::FontWeight::BOLD,
152                FontWeightContent::ExtraBold => gpui::FontWeight::EXTRA_BOLD,
153                FontWeightContent::Black => gpui::FontWeight::BLACK,
154            }),
155            font_style: style.font_style.map(|s| match s {
156                FontStyle::Normal => gpui::FontStyle::Normal,
157                FontStyle::Italic => gpui::FontStyle::Italic,
158                FontStyle::Underline => gpui::FontStyle::Normal,
159            }),
160            ..Default::default()
161        }
162    }
163}
164
165#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
166pub struct SyntaxColors {
167    pub attribute: Option<ThemeStyle>,
168    pub boolean: Option<ThemeStyle>,
169    pub comment: Option<ThemeStyle>,
170    pub comment_doc: Option<ThemeStyle>,
171    pub constant: Option<ThemeStyle>,
172    pub constructor: Option<ThemeStyle>,
173    pub embedded: Option<ThemeStyle>,
174    pub emphasis: Option<ThemeStyle>,
175    #[serde(rename = "emphasis.strong")]
176    pub emphasis_strong: Option<ThemeStyle>,
177    #[serde(rename = "enum")]
178    pub enum_: Option<ThemeStyle>,
179    pub function: Option<ThemeStyle>,
180    pub hint: Option<ThemeStyle>,
181    pub keyword: Option<ThemeStyle>,
182    pub label: Option<ThemeStyle>,
183    #[serde(rename = "link_text")]
184    pub link_text: Option<ThemeStyle>,
185    #[serde(rename = "link_uri")]
186    pub link_uri: Option<ThemeStyle>,
187    pub number: Option<ThemeStyle>,
188    pub operator: Option<ThemeStyle>,
189    pub predictive: Option<ThemeStyle>,
190    pub preproc: Option<ThemeStyle>,
191    pub primary: Option<ThemeStyle>,
192    pub property: Option<ThemeStyle>,
193    pub punctuation: Option<ThemeStyle>,
194    #[serde(rename = "punctuation.bracket")]
195    pub punctuation_bracket: Option<ThemeStyle>,
196    #[serde(rename = "punctuation.delimiter")]
197    pub punctuation_delimiter: Option<ThemeStyle>,
198    #[serde(rename = "punctuation.list_marker")]
199    pub punctuation_list_marker: Option<ThemeStyle>,
200    #[serde(rename = "punctuation.special")]
201    pub punctuation_special: Option<ThemeStyle>,
202    pub string: Option<ThemeStyle>,
203    #[serde(rename = "string.escape")]
204    pub string_escape: Option<ThemeStyle>,
205    #[serde(rename = "string.regex")]
206    pub string_regex: Option<ThemeStyle>,
207    #[serde(rename = "string.special")]
208    pub string_special: Option<ThemeStyle>,
209    #[serde(rename = "string.special.symbol")]
210    pub string_special_symbol: Option<ThemeStyle>,
211    pub tag: Option<ThemeStyle>,
212    #[serde(rename = "tag.doctype")]
213    pub tag_doctype: Option<ThemeStyle>,
214    #[serde(rename = "text.code.span")]
215    pub text_code_span: Option<ThemeStyle>,
216    #[serde(rename = "text.literal")]
217    pub text_literal: Option<ThemeStyle>,
218    pub title: Option<ThemeStyle>,
219    #[serde(rename = "type")]
220    pub type_: Option<ThemeStyle>,
221    pub variable: Option<ThemeStyle>,
222    #[serde(rename = "variable.special")]
223    pub variable_special: Option<ThemeStyle>,
224    pub variant: Option<ThemeStyle>,
225}
226
227impl SyntaxColors {
228    pub fn style(&self, name: &str) -> Option<HighlightStyle> {
229        if name.is_empty() {
230            return None;
231        }
232
233        let style = match name {
234            "attribute" => self.attribute,
235            "boolean" => self.boolean,
236            "comment" => self.comment,
237            "comment.doc" => self.comment_doc,
238            "constant" => self.constant,
239            "constructor" => self.constructor,
240            "embedded" => self.embedded,
241            "emphasis" => self.emphasis,
242            "emphasis.strong" => self.emphasis_strong,
243            "enum" => self.enum_,
244            "function" => self.function,
245            "hint" => self.hint,
246            "keyword" => self.keyword,
247            "label" => self.label,
248            "link_text" => self.link_text,
249            "link_uri" => self.link_uri,
250            "number" => self.number,
251            "operator" => self.operator,
252            "predictive" => self.predictive,
253            "preproc" => self.preproc,
254            "primary" => self.primary,
255            "property" => self.property,
256            "punctuation" => self.punctuation,
257            "punctuation.bracket" => self.punctuation_bracket,
258            "punctuation.delimiter" => self.punctuation_delimiter,
259            "punctuation.list_marker" => self.punctuation_list_marker,
260            "punctuation.special" => self.punctuation_special,
261            "string" => self.string,
262            "string.escape" => self.string_escape,
263            "string.regex" => self.string_regex,
264            "string.special" => self.string_special,
265            "string.special.symbol" => self.string_special_symbol,
266            "tag" => self.tag,
267            "tag.doctype" => self.tag_doctype,
268            "text.code.span" => self.text_code_span,
269            "text.literal" => self.text_literal,
270            "title" => self.title,
271            "type" => self.type_,
272            "variable" => self.variable,
273            "variable.special" => self.variable_special,
274            "variant" => self.variant,
275            _ => None,
276        }
277        .map(|s| s.into());
278
279        if style.is_some() {
280            style
281        } else if name.contains('.') {
282            name.split('.').next().and_then(|prefix| self.style(prefix))
283        } else {
284            None
285        }
286    }
287
288    pub fn style_for_index(&self, index: usize) -> Option<HighlightStyle> {
289        const HIGHLIGHT_NAMES: [&str; 41] = [
290            "attribute",
291            "boolean",
292            "comment",
293            "comment.doc",
294            "constant",
295            "constructor",
296            "embedded",
297            "emphasis",
298            "emphasis.strong",
299            "enum",
300            "function",
301            "hint",
302            "keyword",
303            "label",
304            "link_text",
305            "link_uri",
306            "number",
307            "operator",
308            "predictive",
309            "preproc",
310            "primary",
311            "property",
312            "punctuation",
313            "punctuation.bracket",
314            "punctuation.delimiter",
315            "punctuation.list_marker",
316            "punctuation.special",
317            "string",
318            "string.escape",
319            "string.regex",
320            "string.special",
321            "string.special.symbol",
322            "tag",
323            "tag.doctype",
324            "text.code.span",
325            "text.literal",
326            "title",
327            "type",
328            "variable",
329            "variable.special",
330            "variant",
331        ];
332
333        HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name))
334    }
335}
336
337#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
338pub struct StatusColors {
339    // Minimal stub
340}
341
342impl StatusColors {
343    pub fn error(&self, _cx: &gpui::App) -> gpui::Hsla {
344        gpui::Hsla::default()
345    }
346
347    pub fn error_background(&self, _cx: &gpui::App) -> gpui::Hsla {
348        gpui::Hsla::default()
349    }
350
351    pub fn error_border(&self, _cx: &gpui::App) -> gpui::Hsla {
352        gpui::Hsla::default()
353    }
354
355    pub fn warning(&self, _cx: &gpui::App) -> gpui::Hsla {
356        gpui::Hsla::default()
357    }
358
359    pub fn warning_background(&self, _cx: &gpui::App) -> gpui::Hsla {
360        gpui::Hsla::default()
361    }
362
363    pub fn warning_border(&self, _cx: &gpui::App) -> gpui::Hsla {
364        gpui::Hsla::default()
365    }
366
367    pub fn info(&self, _cx: &gpui::App) -> gpui::Hsla {
368        gpui::Hsla::default()
369    }
370
371    pub fn info_background(&self, _cx: &gpui::App) -> gpui::Hsla {
372        gpui::Hsla::default()
373    }
374
375    pub fn info_border(&self, _cx: &gpui::App) -> gpui::Hsla {
376        gpui::Hsla::default()
377    }
378
379    pub fn success(&self, _cx: &gpui::App) -> gpui::Hsla {
380        gpui::Hsla::default()
381    }
382
383    pub fn success_background(&self, _cx: &gpui::App) -> gpui::Hsla {
384        gpui::Hsla::default()
385    }
386
387    pub fn success_border(&self, _cx: &gpui::App) -> gpui::Hsla {
388        gpui::Hsla::default()
389    }
390
391    pub fn hint(&self, _cx: &gpui::App) -> gpui::Hsla {
392        gpui::Hsla::default()
393    }
394
395    pub fn hint_background(&self, _cx: &gpui::App) -> gpui::Hsla {
396        gpui::Hsla::default()
397    }
398
399    pub fn hint_border(&self, _cx: &gpui::App) -> gpui::Hsla {
400        gpui::Hsla::default()
401    }
402}
403
404#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
405pub struct HighlightThemeStyle {
406    pub editor_background: Option<gpui::Hsla>,
407    pub editor_foreground: Option<gpui::Hsla>,
408    pub editor_active_line: Option<gpui::Hsla>,
409    pub editor_line_number: Option<gpui::Hsla>,
410    pub editor_active_line_number: Option<gpui::Hsla>,
411    pub editor_invisible: Option<gpui::Hsla>,
412    #[serde(rename = "editor.gutter.background")]
413    pub editor_gutter_background: Option<gpui::Hsla>,
414    #[serde(flatten)]
415    pub status: StatusColors,
416    #[serde(rename = "syntax")]
417    pub syntax: SyntaxColors,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
421pub struct HighlightTheme {
422    pub name: String,
423    #[serde(default)]
424    pub appearance: crate::ThemeMode,
425    pub style: HighlightThemeStyle,
426}
427
428impl std::ops::Deref for HighlightTheme {
429    type Target = SyntaxColors;
430
431    fn deref(&self) -> &Self::Target {
432        &self.style.syntax
433    }
434}
435
436impl HighlightTheme {
437    pub fn default_dark() -> std::sync::Arc<Self> {
438        use crate::DEFAULT_THEME_COLORS;
439        DEFAULT_THEME_COLORS[&crate::ThemeMode::Dark].1.clone()
440    }
441
442    pub fn default_light() -> std::sync::Arc<Self> {
443        use crate::DEFAULT_THEME_COLORS;
444        DEFAULT_THEME_COLORS[&crate::ThemeMode::Light].1.clone()
445    }
446}
447
448impl gpui_base::input::HighlightStyleResolver for HighlightTheme {
449    fn style(&self, name: &str) -> Option<HighlightStyle> {
450        self.style.syntax.style(name)
451    }
452}
453
454// Language registry stub
455pub struct LanguageRegistry {
456    languages: Mutex<HashMap<SharedString, GrammarConfig>>,
457}
458
459impl LanguageRegistry {
460    pub fn singleton() -> &'static LazyLock<LanguageRegistry> {
461        static INSTANCE: LazyLock<LanguageRegistry> = LazyLock::new(|| LanguageRegistry {
462            languages: Mutex::new(HashMap::new()),
463        });
464        &INSTANCE
465    }
466
467    pub fn register(&self, lang: &str, config: &GrammarConfig) {
468        self.languages
469            .lock()
470            .unwrap()
471            .insert(lang.to_string().into(), config.clone());
472    }
473
474    pub(crate) fn editing_language_name(&self, name: &str) -> SharedString {
475        self.languages
476            .lock()
477            .unwrap()
478            .get_key_value(name)
479            .map(|(name, _)| name.clone())
480            .unwrap_or_else(|| super::language_name(name))
481    }
482
483    pub fn languages(&self) -> Vec<SharedString> {
484        self.languages.lock().unwrap().keys().cloned().collect()
485    }
486
487    pub fn language(&self, name: &str) -> Option<GrammarConfig> {
488        self.languages.lock().unwrap().get(name).cloned()
489    }
490}
491
492#[cfg(test)]
493mod registry_compat_tests {
494    use super::*;
495
496    #[test]
497    fn registrations_preserve_exact_names_without_alias_fallback() {
498        let registry = LanguageRegistry {
499            languages: Mutex::new(HashMap::new()),
500        };
501        registry.register(
502            "json",
503            &LanguageConfig {
504                name: "canonical".into(),
505            },
506        );
507        assert!(registry.language("jsonc").is_none());
508        registry.register(
509            "jsonc",
510            &LanguageConfig {
511                name: "custom alias".into(),
512            },
513        );
514        registry.register(
515            "JSON",
516            &LanguageConfig {
517                name: "custom uppercase".into(),
518            },
519        );
520        assert_eq!(registry.language("json").unwrap().name, "canonical");
521        assert_eq!(registry.language("jsonc").unwrap().name, "custom alias");
522        assert_eq!(registry.language("JSON").unwrap().name, "custom uppercase");
523        assert!(registry.language("Json").is_none());
524        let mut names = registry.languages();
525        names.sort();
526        assert_eq!(names, vec!["JSON", "json", "jsonc"]);
527        assert_eq!(registry.editing_language_name("jsonc"), "jsonc");
528        assert_eq!(registry.editing_language_name("JSON"), "JSON");
529        assert_eq!(registry.editing_language_name("pyi"), "python");
530    }
531}