1use gpui::{HighlightStyle, SharedString};
9use std::ops::Range;
10use std::time::Duration;
11
12pub 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 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 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 }
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#[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#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct LanguageConfig {
107 pub name: SharedString,
108}
109
110pub type GrammarConfig = LanguageConfig;
112
113impl LanguageConfig {
114 pub fn has_grammar(&self) -> bool {
115 false
116 }
117}
118
119use 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 }
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
472pub 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}