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