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