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)]
152#[non_exhaustive]
153pub struct ThemeStyle {
154    pub color: Option<gpui::Hsla>,
155    pub font_style: Option<FontStyle>,
156    pub font_weight: Option<FontWeightContent>,
157}
158
159impl From<ThemeStyle> for HighlightStyle {
160    fn from(style: ThemeStyle) -> Self {
161        HighlightStyle {
162            color: style.color,
163            font_weight: style.font_weight.map(|w| match w {
164                FontWeightContent::Thin => gpui::FontWeight::THIN,
165                FontWeightContent::ExtraLight => gpui::FontWeight::EXTRA_LIGHT,
166                FontWeightContent::Light => gpui::FontWeight::LIGHT,
167                FontWeightContent::Normal => gpui::FontWeight::NORMAL,
168                FontWeightContent::Medium => gpui::FontWeight::MEDIUM,
169                FontWeightContent::Semibold => gpui::FontWeight::SEMIBOLD,
170                FontWeightContent::Bold => gpui::FontWeight::BOLD,
171                FontWeightContent::ExtraBold => gpui::FontWeight::EXTRA_BOLD,
172                FontWeightContent::Black => gpui::FontWeight::BLACK,
173            }),
174            font_style: style.font_style.map(|s| match s {
175                FontStyle::Normal => gpui::FontStyle::Normal,
176                FontStyle::Italic => gpui::FontStyle::Italic,
177                FontStyle::Underline => gpui::FontStyle::Normal,
178            }),
179            ..Default::default()
180        }
181    }
182}
183
184#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
185#[non_exhaustive]
186pub struct SyntaxColors {
187    pub attribute: Option<ThemeStyle>,
188    pub boolean: Option<ThemeStyle>,
189    pub comment: Option<ThemeStyle>,
190    pub comment_doc: Option<ThemeStyle>,
191    pub constant: Option<ThemeStyle>,
192    pub constructor: Option<ThemeStyle>,
193    pub embedded: Option<ThemeStyle>,
194    pub emphasis: Option<ThemeStyle>,
195    #[serde(rename = "emphasis.strong")]
196    pub emphasis_strong: Option<ThemeStyle>,
197    #[serde(rename = "enum")]
198    pub enum_: Option<ThemeStyle>,
199    pub function: Option<ThemeStyle>,
200    pub hint: Option<ThemeStyle>,
201    pub keyword: Option<ThemeStyle>,
202    pub label: Option<ThemeStyle>,
203    #[serde(rename = "link_text")]
204    pub link_text: Option<ThemeStyle>,
205    #[serde(rename = "link_uri")]
206    pub link_uri: Option<ThemeStyle>,
207    pub number: Option<ThemeStyle>,
208    pub operator: Option<ThemeStyle>,
209    pub predictive: Option<ThemeStyle>,
210    pub preproc: Option<ThemeStyle>,
211    pub primary: Option<ThemeStyle>,
212    pub property: Option<ThemeStyle>,
213    pub punctuation: Option<ThemeStyle>,
214    #[serde(rename = "punctuation.bracket")]
215    pub punctuation_bracket: Option<ThemeStyle>,
216    #[serde(rename = "punctuation.delimiter")]
217    pub punctuation_delimiter: Option<ThemeStyle>,
218    #[serde(rename = "punctuation.list_marker")]
219    pub punctuation_list_marker: Option<ThemeStyle>,
220    #[serde(rename = "punctuation.special")]
221    pub punctuation_special: Option<ThemeStyle>,
222    pub string: Option<ThemeStyle>,
223    #[serde(rename = "string.escape")]
224    pub string_escape: Option<ThemeStyle>,
225    #[serde(rename = "string.regex")]
226    pub string_regex: Option<ThemeStyle>,
227    #[serde(rename = "string.special")]
228    pub string_special: Option<ThemeStyle>,
229    #[serde(rename = "string.special.symbol")]
230    pub string_special_symbol: Option<ThemeStyle>,
231    pub tag: Option<ThemeStyle>,
232    #[serde(rename = "tag.doctype")]
233    pub tag_doctype: Option<ThemeStyle>,
234    #[serde(rename = "text.code.span")]
235    pub text_code_span: Option<ThemeStyle>,
236    #[serde(rename = "text.literal")]
237    pub text_literal: Option<ThemeStyle>,
238    pub title: Option<ThemeStyle>,
239    #[serde(rename = "type")]
240    pub type_: Option<ThemeStyle>,
241    pub variable: Option<ThemeStyle>,
242    #[serde(rename = "variable.special")]
243    pub variable_special: Option<ThemeStyle>,
244    pub variant: Option<ThemeStyle>,
245}
246
247impl SyntaxColors {
248    pub fn style(&self, name: &str) -> Option<HighlightStyle> {
249        if name.is_empty() {
250            return None;
251        }
252
253        let style = match name {
254            "attribute" => self.attribute,
255            "boolean" => self.boolean,
256            "comment" => self.comment,
257            "comment.doc" => self.comment_doc,
258            "constant" => self.constant,
259            "constructor" => self.constructor,
260            "embedded" => self.embedded,
261            "emphasis" => self.emphasis,
262            "emphasis.strong" => self.emphasis_strong,
263            "enum" => self.enum_,
264            "function" => self.function,
265            "hint" => self.hint,
266            "keyword" => self.keyword,
267            "label" => self.label,
268            "link_text" => self.link_text,
269            "link_uri" => self.link_uri,
270            "number" => self.number,
271            "operator" => self.operator,
272            "predictive" => self.predictive,
273            "preproc" => self.preproc,
274            "primary" => self.primary,
275            "property" => self.property,
276            "punctuation" => self.punctuation,
277            "punctuation.bracket" => self.punctuation_bracket,
278            "punctuation.delimiter" => self.punctuation_delimiter,
279            "punctuation.list_marker" => self.punctuation_list_marker,
280            "punctuation.special" => self.punctuation_special,
281            "string" => self.string,
282            "string.escape" => self.string_escape,
283            "string.regex" => self.string_regex,
284            "string.special" => self.string_special,
285            "string.special.symbol" => self.string_special_symbol,
286            "tag" => self.tag,
287            "tag.doctype" => self.tag_doctype,
288            "text.code.span" => self.text_code_span,
289            "text.literal" => self.text_literal,
290            "title" => self.title,
291            "type" => self.type_,
292            "variable" => self.variable,
293            "variable.special" => self.variable_special,
294            "variant" => self.variant,
295            _ => None,
296        }
297        .map(|s| s.into());
298
299        if style.is_some() {
300            style
301        } else if name.contains('.') {
302            name.split('.').next().and_then(|prefix| self.style(prefix))
303        } else {
304            None
305        }
306    }
307
308    pub fn style_for_index(&self, index: usize) -> Option<HighlightStyle> {
309        const HIGHLIGHT_NAMES: [&str; 41] = [
310            "attribute",
311            "boolean",
312            "comment",
313            "comment.doc",
314            "constant",
315            "constructor",
316            "embedded",
317            "emphasis",
318            "emphasis.strong",
319            "enum",
320            "function",
321            "hint",
322            "keyword",
323            "label",
324            "link_text",
325            "link_uri",
326            "number",
327            "operator",
328            "predictive",
329            "preproc",
330            "primary",
331            "property",
332            "punctuation",
333            "punctuation.bracket",
334            "punctuation.delimiter",
335            "punctuation.list_marker",
336            "punctuation.special",
337            "string",
338            "string.escape",
339            "string.regex",
340            "string.special",
341            "string.special.symbol",
342            "tag",
343            "tag.doctype",
344            "text.code.span",
345            "text.literal",
346            "title",
347            "type",
348            "variable",
349            "variable.special",
350            "variant",
351        ];
352
353        HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name))
354    }
355}
356
357#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
358pub struct StatusColors {
359    // Minimal stub
360}
361
362impl StatusColors {
363    pub fn error(&self, _cx: &gpui::App) -> gpui::Hsla {
364        gpui::Hsla::default()
365    }
366
367    pub fn error_background(&self, _cx: &gpui::App) -> gpui::Hsla {
368        gpui::Hsla::default()
369    }
370
371    pub fn error_border(&self, _cx: &gpui::App) -> gpui::Hsla {
372        gpui::Hsla::default()
373    }
374
375    pub fn warning(&self, _cx: &gpui::App) -> gpui::Hsla {
376        gpui::Hsla::default()
377    }
378
379    pub fn warning_background(&self, _cx: &gpui::App) -> gpui::Hsla {
380        gpui::Hsla::default()
381    }
382
383    pub fn warning_border(&self, _cx: &gpui::App) -> gpui::Hsla {
384        gpui::Hsla::default()
385    }
386
387    pub fn info(&self, _cx: &gpui::App) -> gpui::Hsla {
388        gpui::Hsla::default()
389    }
390
391    pub fn info_background(&self, _cx: &gpui::App) -> gpui::Hsla {
392        gpui::Hsla::default()
393    }
394
395    pub fn info_border(&self, _cx: &gpui::App) -> gpui::Hsla {
396        gpui::Hsla::default()
397    }
398
399    pub fn success(&self, _cx: &gpui::App) -> gpui::Hsla {
400        gpui::Hsla::default()
401    }
402
403    pub fn success_background(&self, _cx: &gpui::App) -> gpui::Hsla {
404        gpui::Hsla::default()
405    }
406
407    pub fn success_border(&self, _cx: &gpui::App) -> gpui::Hsla {
408        gpui::Hsla::default()
409    }
410
411    pub fn hint(&self, _cx: &gpui::App) -> gpui::Hsla {
412        gpui::Hsla::default()
413    }
414
415    pub fn hint_background(&self, _cx: &gpui::App) -> gpui::Hsla {
416        gpui::Hsla::default()
417    }
418
419    pub fn hint_border(&self, _cx: &gpui::App) -> gpui::Hsla {
420        gpui::Hsla::default()
421    }
422
423    pub fn created(&self, _cx: &gpui::App) -> gpui::Hsla {
424        gpui::Hsla::default()
425    }
426
427    pub fn modified(&self, _cx: &gpui::App) -> gpui::Hsla {
428        gpui::Hsla::default()
429    }
430
431    pub fn deleted(&self, _cx: &gpui::App) -> gpui::Hsla {
432        gpui::Hsla::default()
433    }
434}
435
436#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
437#[non_exhaustive]
438pub struct HighlightThemeStyle {
439    pub editor_background: Option<gpui::Hsla>,
440    pub editor_foreground: Option<gpui::Hsla>,
441    pub editor_active_line: Option<gpui::Hsla>,
442    pub editor_line_number: Option<gpui::Hsla>,
443    pub editor_active_line_number: Option<gpui::Hsla>,
444    pub editor_invisible: Option<gpui::Hsla>,
445    #[serde(rename = "editor.gutter.background")]
446    pub editor_gutter_background: Option<gpui::Hsla>,
447    #[serde(flatten)]
448    pub status: StatusColors,
449    #[serde(rename = "syntax")]
450    pub syntax: SyntaxColors,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
454#[non_exhaustive]
455pub struct HighlightTheme {
456    pub name: String,
457    #[serde(default)]
458    pub appearance: crate::ThemeMode,
459    pub style: HighlightThemeStyle,
460}
461
462impl std::ops::Deref for HighlightTheme {
463    type Target = SyntaxColors;
464
465    fn deref(&self) -> &Self::Target {
466        &self.style.syntax
467    }
468}
469
470impl HighlightTheme {
471    pub fn default_dark() -> std::sync::Arc<Self> {
472        use crate::DEFAULT_THEME_COLORS;
473        DEFAULT_THEME_COLORS[&crate::ThemeMode::Dark].1.clone()
474    }
475
476    pub fn default_light() -> std::sync::Arc<Self> {
477        use crate::DEFAULT_THEME_COLORS;
478        DEFAULT_THEME_COLORS[&crate::ThemeMode::Light].1.clone()
479    }
480}
481
482impl gpui_base::input::HighlightStyleResolver for HighlightTheme {
483    fn style(&self, name: &str) -> Option<HighlightStyle> {
484        self.style.syntax.style(name)
485    }
486}
487
488// Language registry stub
489pub struct LanguageRegistry {
490    languages: Mutex<HashMap<SharedString, GrammarConfig>>,
491}
492
493impl LanguageRegistry {
494    pub fn singleton() -> &'static LazyLock<LanguageRegistry> {
495        static INSTANCE: LazyLock<LanguageRegistry> = LazyLock::new(|| LanguageRegistry {
496            languages: Mutex::new(HashMap::new()),
497        });
498        &INSTANCE
499    }
500
501    pub fn register(&self, lang: &str, config: &GrammarConfig) {
502        self.languages
503            .lock()
504            .unwrap()
505            .insert(lang.to_string().into(), config.clone());
506    }
507
508    pub(crate) fn editing_language_name(&self, name: &str) -> SharedString {
509        self.languages
510            .lock()
511            .unwrap()
512            .get_key_value(name)
513            .map(|(name, _)| name.clone())
514            .unwrap_or_else(|| super::language_name(name))
515    }
516
517    pub fn languages(&self) -> Vec<SharedString> {
518        self.languages.lock().unwrap().keys().cloned().collect()
519    }
520
521    pub fn language(&self, name: &str) -> Option<GrammarConfig> {
522        self.languages.lock().unwrap().get(name).cloned()
523    }
524}
525
526#[cfg(test)]
527mod registry_compat_tests {
528    use super::*;
529
530    #[test]
531    fn registrations_preserve_exact_names_without_alias_fallback() {
532        let registry = LanguageRegistry {
533            languages: Mutex::new(HashMap::new()),
534        };
535        registry.register(
536            "json",
537            &LanguageConfig {
538                name: "canonical".into(),
539            },
540        );
541        assert!(registry.language("jsonc").is_none());
542        registry.register(
543            "jsonc",
544            &LanguageConfig {
545                name: "custom alias".into(),
546            },
547        );
548        registry.register(
549            "JSON",
550            &LanguageConfig {
551                name: "custom uppercase".into(),
552            },
553        );
554        assert_eq!(registry.language("json").unwrap().name, "canonical");
555        assert_eq!(registry.language("jsonc").unwrap().name, "custom alias");
556        assert_eq!(registry.language("JSON").unwrap().name, "custom uppercase");
557        assert!(registry.language("Json").is_none());
558        let mut names = registry.languages();
559        names.sort();
560        assert_eq!(names, vec!["JSON", "json", "jsonc"]);
561        assert_eq!(registry.editing_language_name("jsonc"), "jsonc");
562        assert_eq!(registry.editing_language_name("JSON"), "JSON");
563        assert_eq!(registry.editing_language_name("pyi"), "python");
564    }
565}