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)]
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 }
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
488pub 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}