Skip to main content

gpui_component/highlighter/
wasm_stub.rs

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