1use std::collections::{HashMap, HashSet};
53use std::sync::atomic::{AtomicBool, Ordering};
54use std::sync::{Arc, Mutex, OnceLock};
55
56use arc_swap::ArcSwap;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
62pub struct LanguageName(&'static str);
63
64impl LanguageName {
65 pub fn intern(name: &str) -> Self {
70 static NAMES: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
71 let names = NAMES.get_or_init(|| Mutex::new(HashSet::new()));
72 let mut names = names.lock().unwrap_or_else(|e| e.into_inner());
73 if let Some(existing) = names.get(name) {
74 return Self(existing);
75 }
76 let leaked: &'static str = Box::leak(name.to_owned().into_boxed_str());
80 names.insert(leaked);
81 Self(leaked)
82 }
83
84 pub fn as_str(self) -> &'static str {
85 self.0
86 }
87}
88
89impl std::fmt::Display for LanguageName {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(self.0)
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum LanguageRegistrationError {
102 ShadowsBuiltin { name: String },
107 AlreadyRegistered { name: String },
109 NoExtensions { name: String },
113 QueryCompile { name: String, detail: String },
121}
122
123impl std::fmt::Display for LanguageRegistrationError {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 match self {
126 Self::ShadowsBuiltin { name } => write!(
127 f,
128 "language '{name}' is built into the editor and cannot be replaced by a plugin"
129 ),
130 Self::AlreadyRegistered { name } => {
131 write!(f, "language '{name}' is already registered by a plugin")
132 }
133 Self::NoExtensions { name } => {
134 write!(f, "language '{name}' registered no file extensions")
135 }
136 Self::QueryCompile { name, detail } => {
137 write!(f, "language '{name}': {detail}")
138 }
139 }
140 }
141}
142
143impl std::error::Error for LanguageRegistrationError {}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct LanguageRegistration {
152 pub name: LanguageName,
153 pub extensions: Vec<String>,
155 pub provenance: u64,
160}
161
162#[derive(Debug, Default, Clone)]
169pub struct PluginLanguages {
170 by_extension: HashMap<String, LanguageName>,
171 registrations: Vec<Arc<LanguageRegistration>>,
172}
173
174impl PluginLanguages {
175 pub fn resolve_extension(&self, ext: &str) -> Option<LanguageName> {
176 self.by_extension.get(&ext.to_ascii_lowercase()).copied()
177 }
178
179 pub fn get(&self, name: LanguageName) -> Option<&Arc<LanguageRegistration>> {
180 self.registrations.iter().find(|r| r.name == name)
181 }
182
183 pub fn iter(&self) -> impl Iterator<Item = &Arc<LanguageRegistration>> {
184 self.registrations.iter()
185 }
186
187 pub fn is_empty(&self) -> bool {
188 self.registrations.is_empty()
189 }
190}
191
192pub type PluginLanguagesHandle = Arc<ArcSwap<PluginLanguages>>;
195
196static ANY_REGISTERED: AtomicBool = AtomicBool::new(false);
208
209pub fn handle() -> &'static PluginLanguagesHandle {
212 static HANDLE: OnceLock<PluginLanguagesHandle> = OnceLock::new();
213 HANDLE.get_or_init(|| Arc::new(ArcSwap::from_pointee(PluginLanguages::default())))
214}
215
216pub fn resolve_extension(ext: &str) -> Option<LanguageName> {
223 if !ANY_REGISTERED.load(Ordering::Relaxed) {
224 return None;
225 }
226 handle().load().resolve_extension(ext)
227}
228
229pub fn snapshot() -> Arc<PluginLanguages> {
232 handle().load_full()
233}
234
235pub fn register(
240 name: &str,
241 extensions: &[&str],
242 provenance: u64,
243) -> Result<LanguageName, LanguageRegistrationError> {
244 let normalised = validate(name, extensions)?;
245 let interned = LanguageName::intern(name);
246 claim(interned, normalised, provenance).map_err(|_| {
247 LanguageRegistrationError::AlreadyRegistered {
248 name: name.to_owned(),
249 }
250 })?;
251 Ok(interned)
252}
253
254fn validate(name: &str, extensions: &[&str]) -> Result<Vec<String>, LanguageRegistrationError> {
257 if crate::Lang::builtin_by_name(name).is_some() {
258 return Err(LanguageRegistrationError::ShadowsBuiltin {
259 name: name.to_owned(),
260 });
261 }
262 let normalised: Vec<String> = extensions
263 .iter()
264 .map(|e| e.trim_start_matches('.').to_ascii_lowercase())
265 .filter(|e| !e.is_empty())
266 .collect();
267 if normalised.is_empty() {
268 return Err(LanguageRegistrationError::NoExtensions {
269 name: name.to_owned(),
270 });
271 }
272 Ok(normalised)
273}
274
275fn claim(interned: LanguageName, extensions: Vec<String>, provenance: u64) -> Result<(), ()> {
278 let registration = Arc::new(LanguageRegistration {
279 name: interned,
280 extensions,
281 provenance,
282 });
283 let mut refused = false;
288 handle().rcu(|current| {
289 refused = current.registrations.iter().any(|r| r.name == interned);
290 if refused {
291 return PluginLanguages::clone(current);
292 }
293 let mut next = PluginLanguages::clone(current);
294 for ext in ®istration.extensions {
295 next.by_extension.insert(ext.clone(), interned);
298 }
299 next.registrations.push(Arc::clone(®istration));
300 next
301 });
302 if refused {
303 return Err(());
304 }
305 ANY_REGISTERED.store(true, Ordering::Relaxed);
306 Ok(())
307}
308
309pub fn register_with_grammar(
322 name: &str,
323 extensions: &[&str],
324 grammar: &crate::registry::GrammarSpec,
325 provenance: u64,
326) -> Result<LanguageName, LanguageRegistrationError> {
327 register_with_grammar_themed(name, extensions, grammar, provenance, None)
328}
329
330pub fn register_with_grammar_themed(
345 name: &str,
346 extensions: &[&str],
347 grammar: &crate::registry::GrammarSpec,
348 provenance: u64,
349 theme: Option<&dyn lattice_theme::ThemeRegistry>,
350) -> Result<LanguageName, LanguageRegistrationError> {
351 let normalised = validate(name, extensions)?;
352 let interned = LanguageName::intern(name);
353
354 let config =
357 crate::registry::compile_plugin_config(interned.as_str(), grammar, provenance, theme)
358 .map_err(|e| LanguageRegistrationError::QueryCompile {
359 name: name.to_owned(),
360 detail: e.to_string(),
361 })?;
362
363 claim(interned, normalised, provenance).map_err(|_| {
364 LanguageRegistrationError::AlreadyRegistered {
365 name: name.to_owned(),
366 }
367 })?;
368 crate::registry::install_plugin_config(interned.as_str(), config);
369 Ok(interned)
370}
371
372pub fn unregister_plugin(provenance: u64) -> usize {
380 crate::registry::unregister_plugin(provenance);
386 let mut removed = 0;
387 handle().rcu(|current| {
388 let mut next = PluginLanguages::clone(current);
389 let before = next.registrations.len();
390 next.registrations.retain(|r| r.provenance != provenance);
391 removed = before - next.registrations.len();
392 next.by_extension.clear();
397 for reg in &next.registrations {
398 for ext in ®.extensions {
399 next.by_extension.insert(ext.clone(), reg.name);
400 }
401 }
402 next
403 });
404 if removed > 0 && handle().load().registrations.is_empty() {
405 ANY_REGISTERED.store(false, Ordering::Relaxed);
406 }
407 removed
408}
409
410#[cfg(test)]
411mod tests {
412 #![allow(clippy::unwrap_used, clippy::panic)]
413 use super::*;
414 use crate::Lang;
415 use std::path::PathBuf;
416 use std::sync::atomic::AtomicU64;
417
418 fn unique(tag: &str) -> (String, String, u64) {
423 static N: AtomicU64 = AtomicU64::new(0);
424 let n = N.fetch_add(1, Ordering::Relaxed);
425 (
426 format!("lg2{tag}{n}"),
427 format!("lg2x{tag}{n}"),
428 9_000_000 + n,
429 )
430 }
431
432 #[test]
433 fn registered_language_resolves_by_extension() {
434 let (name, ext, plugin) = unique("res");
435 let interned = register(&name, &[&ext], plugin).unwrap();
436
437 let path = PathBuf::from(format!("notes.{ext}"));
438 assert_eq!(
439 Lang::detect_from_path(Some(&path)),
440 Lang::Plugin(interned),
441 "a registered extension should resolve to its plugin language"
442 );
443 assert_eq!(Lang::Plugin(interned).name(), name);
444 assert_eq!(Lang::Plugin(interned).label(), name);
445
446 unregister_plugin(plugin);
447 }
448
449 #[test]
450 fn unload_withdraws_the_language() {
451 let (name, ext, plugin) = unique("unload");
452 register(&name, &[&ext], plugin).unwrap();
453 let path = PathBuf::from(format!("notes.{ext}"));
454 assert!(matches!(
455 Lang::detect_from_path(Some(&path)),
456 Lang::Plugin(_)
457 ));
458
459 assert_eq!(unregister_plugin(plugin), 1);
460 assert_eq!(
461 Lang::detect_from_path(Some(&path)),
462 Lang::Plain,
463 "after unload the extension must fall back to plain"
464 );
465 }
466
467 #[test]
468 fn unload_withdraws_every_language_from_that_plugin() {
469 let (a, ext_a, plugin) = unique("multia");
470 let (b, ext_b, _) = unique("multib");
471 register(&a, &[&ext_a], plugin).unwrap();
472 register(&b, &[&ext_b], plugin).unwrap();
473
474 assert_eq!(unregister_plugin(plugin), 2, "both, without a token list");
475 for ext in [&ext_a, &ext_b] {
476 assert_eq!(
477 Lang::detect_from_path(Some(&PathBuf::from(format!("f.{ext}")))),
478 Lang::Plain
479 );
480 }
481 }
482
483 #[test]
484 fn unloading_one_plugin_leaves_anothers_language_alone() {
485 let (a, ext_a, plugin_a) = unique("isoa");
486 let (b, ext_b, plugin_b) = unique("isob");
487 let name_b = register(&b, &[&ext_b], plugin_b).unwrap();
488 register(&a, &[&ext_a], plugin_a).unwrap();
489
490 unregister_plugin(plugin_a);
491 assert_eq!(
492 Lang::detect_from_path(Some(&PathBuf::from(format!("f.{ext_b}")))),
493 Lang::Plugin(name_b),
494 "withdrawing one plugin must not rebuild away another's extensions"
495 );
496 unregister_plugin(plugin_b);
497 }
498
499 #[test]
500 fn plugin_and_native_languages_coexist() {
501 let (name, ext, plugin) = unique("coexist");
502 let interned = register(&name, &[&ext], plugin).unwrap();
503
504 assert_eq!(
505 Lang::detect_from_path(Some(&PathBuf::from("main.rs"))),
506 Lang::Rust
507 );
508 assert_eq!(
509 Lang::detect_from_path(Some(&PathBuf::from(format!("a.{ext}")))),
510 Lang::Plugin(interned)
511 );
512 unregister_plugin(plugin);
513 }
514
515 #[test]
516 fn native_resolution_wins_over_a_plugin_claiming_the_same_extension() {
517 let (name, _, plugin) = unique("shadowext");
518 register(&name, &["rs"], plugin).unwrap();
522 assert_eq!(
523 Lang::detect_from_path(Some(&PathBuf::from("main.rs"))),
524 Lang::Rust
525 );
526 unregister_plugin(plugin);
527 }
528
529 #[test]
530 fn native_resolution_is_unchanged_when_the_registry_is_empty() {
531 assert_eq!(
536 Lang::detect_from_path(Some(&PathBuf::from("a.py"))),
537 Lang::Python
538 );
539 assert_eq!(
540 Lang::detect_from_path(Some(&PathBuf::from("a.md"))),
541 Lang::Markdown
542 );
543 assert_eq!(Lang::detect_from_path(None), Lang::Plain);
544 assert_eq!(
545 Lang::detect_from_path(Some(&PathBuf::from("a.lg2-never-registered"))),
546 Lang::Plain
547 );
548 }
549
550 #[test]
551 fn a_plugin_may_not_shadow_a_builtin_name() {
552 let err = register("rust", &["myrust"], 9_999_001).unwrap_err();
553 assert_eq!(
554 err,
555 LanguageRegistrationError::ShadowsBuiltin {
556 name: "rust".into()
557 }
558 );
559 assert_eq!(
561 Lang::detect_from_path(Some(&PathBuf::from("a.myrust"))),
562 Lang::Plain
563 );
564 }
565
566 #[test]
567 fn a_name_may_not_be_registered_twice() {
568 let (name, ext, plugin) = unique("dup");
569 register(&name, &[&ext], plugin).unwrap();
570 let err = register(&name, &["otherext"], plugin + 1).unwrap_err();
571 assert_eq!(
572 err,
573 LanguageRegistrationError::AlreadyRegistered { name: name.clone() }
574 );
575 unregister_plugin(plugin);
576 }
577
578 #[test]
579 fn a_language_with_no_extensions_is_refused() {
580 let (name, _, plugin) = unique("noext");
581 assert_eq!(
582 register(&name, &[], plugin).unwrap_err(),
583 LanguageRegistrationError::NoExtensions { name: name.clone() }
584 );
585 assert_eq!(
587 register(&name, &["."], plugin).unwrap_err(),
588 LanguageRegistrationError::NoExtensions { name }
589 );
590 }
591
592 #[test]
593 fn extensions_are_matched_case_insensitively_and_dots_are_optional() {
594 let (name, ext, plugin) = unique("case");
595 let interned = register(&name, &[&format!(".{}", ext.to_uppercase())], plugin).unwrap();
596 assert_eq!(
597 Lang::detect_from_path(Some(&PathBuf::from(format!("F.{}", ext.to_uppercase())))),
598 Lang::Plugin(interned)
599 );
600 assert_eq!(
601 Lang::detect_from_path(Some(&PathBuf::from(format!("f.{ext}")))),
602 Lang::Plugin(interned)
603 );
604 unregister_plugin(plugin);
605 }
606
607 #[test]
608 fn interning_the_same_name_twice_yields_one_pointer() {
609 let a = LanguageName::intern("lg2-intern-probe");
610 let b = LanguageName::intern("lg2-intern-probe");
611 assert_eq!(a, b);
612 assert!(
613 std::ptr::eq(a.as_str(), b.as_str()),
614 "re-interning must not leak a second copy"
615 );
616 }
617
618 fn json_grammar(highlights: &str) -> crate::registry::GrammarSpec {
628 crate::registry::GrammarSpec {
629 grammar: tree_sitter_json::LANGUAGE.into(),
630 highlights: Some(highlights.to_string()),
631 folds: None,
632 injections: None,
633 indents: None,
634 textobjects: None,
635 conceal_rules: vec![],
636 }
637 }
638
639 #[test]
644 fn a_registered_language_parses_and_highlights_through_the_normal_path() {
645 let (name, ext, plugin) = unique("full");
646 let interned =
647 register_with_grammar(&name, &[&ext], &json_grammar("(string) @string"), plugin)
648 .expect("registers");
649
650 let lang = Lang::detect_from_path(Some(&PathBuf::from(format!("a.{ext}"))));
651 assert_eq!(lang, Lang::Plugin(interned));
652
653 let mut syntax = crate::Syntax::for_language(lang)
654 .expect("registry")
655 .expect("a registered language must yield a Syntax");
656 syntax.parse("{\"k\": \"v\"}");
657 let lines = syntax.highlight_lines_native(0, 1).expect("highlights");
658 assert!(
659 lines[0].iter().any(|s| s.style == crate::Style::String),
660 "the plugin language's highlights query must produce spans: {:?}",
661 lines[0]
662 );
663
664 unregister_plugin(plugin);
665 }
666
667 #[test]
668 fn unload_reverts_an_open_buffers_language_to_plain() {
669 let (name, ext, plugin) = unique("revert");
670 let interned =
671 register_with_grammar(&name, &[&ext], &json_grammar("(string) @string"), plugin)
672 .expect("registers");
673 assert!(
674 crate::Syntax::for_language(Lang::Plugin(interned))
675 .expect("registry")
676 .is_some()
677 );
678
679 unregister_plugin(plugin);
680
681 assert!(
685 crate::Syntax::for_language(Lang::Plugin(interned))
686 .expect("registry")
687 .is_none(),
688 "the grammar must be withdrawn with the language"
689 );
690 }
691
692 #[test]
697 fn a_malformed_query_is_rejected_at_registration_and_names_itself() {
698 let (name, ext, plugin) = unique("badquery");
699 let mut spec = json_grammar("(string) @string");
700 spec.folds = Some("(this_node_does_not_exist) @fold".to_string());
701
702 let err = register_with_grammar(&name, &[&ext], &spec, plugin).unwrap_err();
703 let LanguageRegistrationError::QueryCompile { name: n, detail } = &err else {
704 panic!("expected QueryCompile, got {err:?}");
705 };
706 assert_eq!(n, &name);
707 assert!(
708 detail.contains("folds.scm"),
709 "the offending query must be named: {detail}"
710 );
711
712 assert_eq!(
715 Lang::detect_from_path(Some(&PathBuf::from(format!("a.{ext}")))),
716 Lang::Plain
717 );
718 assert!(
719 crate::Syntax::for_language(Lang::Plugin(LanguageName::intern(&name)))
720 .expect("registry")
721 .is_none()
722 );
723 }
724
725 #[test]
728 fn h2_conceal_rules_survive_registration_and_a_bad_one_drops_alone() {
729 let (name, ext, plugin) = unique("conceal");
730 let mut spec = json_grammar("(string) @string");
731 spec.conceal_rules = vec![
732 (r"(\[\[[^]]+\]\[)[^]]+(\]\])".to_string(), vec![1, 2], None),
733 ("(unclosed".to_string(), vec![1], None),
735 (r"(\[\[)([^]]+)(\]\])".to_string(), vec![4], None),
737 (r"(\[\[)([^]]+)(\]\])".to_string(), vec![1, 3], None),
738 ];
739 let interned = register_with_grammar(&name, &[&ext], &spec, plugin).expect("registers");
740 let reg = crate::registry::live().expect("registry");
741 let rules = reg.conceal_rules(interned.as_str());
742 assert_eq!(rules.len(), 2, "the two good rules survive, the two do not");
743 assert_eq!(rules[0].hide(), &[1, 2]);
744 assert_eq!(rules[1].hide(), &[1, 3]);
745 unregister_plugin(plugin);
746 }
747
748 #[test]
753 fn h2_unloading_the_plugin_takes_its_conceal_rules() {
754 let (name, ext, plugin) = unique("conceal-teardown");
755 let mut spec = json_grammar("(string) @string");
756 spec.conceal_rules = vec![(r"(\[\[)([^]]+)(\]\])".to_string(), vec![1, 3], None)];
757 let interned = register_with_grammar(&name, &[&ext], &spec, plugin).expect("registers");
758 assert_eq!(
759 crate::registry::live()
760 .expect("registry")
761 .conceal_rules(interned.as_str())
762 .len(),
763 1
764 );
765 unregister_plugin(plugin);
766 assert!(
767 crate::registry::live()
768 .expect("registry")
769 .conceal_rules(interned.as_str())
770 .is_empty(),
771 "rules must go when the language does"
772 );
773 }
774
775 #[test]
779 fn h2_a_language_with_no_rules_answers_empty() {
780 let reg = crate::registry::live().expect("registry");
781 assert!(reg.conceal_rules("rust").is_empty());
782 assert!(reg.conceal_rules("not-a-language").is_empty());
783 }
784
785 #[test]
788 fn absent_optional_queries_are_not_an_error() {
789 let (name, ext, plugin) = unique("noopt");
790 let spec = crate::registry::GrammarSpec {
791 grammar: tree_sitter_json::LANGUAGE.into(),
792 highlights: None,
793 folds: None,
794 injections: None,
795 indents: None,
796 textobjects: None,
797 conceal_rules: vec![],
798 };
799 let interned = register_with_grammar(&name, &[&ext], &spec, plugin).expect("registers");
800 let mut syntax = crate::Syntax::for_language(Lang::Plugin(interned))
801 .expect("registry")
802 .expect("still a usable language");
803 syntax.parse("{\"k\": 1}");
804 assert!(syntax.highlight_lines_native(0, 1).expect("ok")[0].is_empty());
806 unregister_plugin(plugin);
807 }
808
809 #[test]
812 fn unloading_a_plugin_leaves_bundled_languages_intact() {
813 let (name, ext, plugin) = unique("bundled");
814 register_with_grammar(&name, &[&ext], &json_grammar("(string) @string"), plugin)
815 .expect("registers");
816 unregister_plugin(plugin);
817
818 for l in [Lang::Rust, Lang::Json, Lang::Markdown] {
819 assert!(
820 crate::Syntax::for_language(l).expect("registry").is_some(),
821 "{l:?} must survive a plugin unload"
822 );
823 }
824 }
825
826 #[test]
829 fn a_losing_registration_does_not_clobber_the_winners_grammar() {
830 let (name, ext, plugin) = unique("clobber");
831 let interned =
832 register_with_grammar(&name, &[&ext], &json_grammar("(string) @string"), plugin)
833 .expect("first wins");
834
835 let mut other = json_grammar("");
836 other.grammar = tree_sitter_md::LANGUAGE.into();
837 let err =
838 register_with_grammar(&name, &["otherclobberext"], &other, plugin + 1).unwrap_err();
839 assert!(matches!(
840 err,
841 LanguageRegistrationError::AlreadyRegistered { .. }
842 ));
843
844 let mut syntax = crate::Syntax::for_language(Lang::Plugin(interned))
847 .expect("registry")
848 .expect("present");
849 syntax.parse("{\"k\": \"v\"}");
850 assert!(
851 syntax.highlight_lines_native(0, 1).expect("ok")[0]
852 .iter()
853 .any(|s| s.style == crate::Style::String),
854 "the winner's highlights query must still be installed"
855 );
856 unregister_plugin(plugin);
857 }
858
859 #[test]
860 fn unregistering_an_unknown_plugin_is_a_no_op() {
861 assert_eq!(unregister_plugin(9_999_999), 0);
862 }
863
864 #[test]
865 fn a_stale_lang_still_names_itself_after_unload() {
866 let (name, ext, plugin) = unique("stale");
867 let interned = register(&name, &[&ext], plugin).unwrap();
868 let held = Lang::Plugin(interned);
869 unregister_plugin(plugin);
870
871 assert_eq!(held.name(), name);
875 assert_eq!(held.comment_syntax().line, None);
876 assert_eq!(crate::major_mode_id_for_lang(held), None);
877 }
878}