1use std::collections::HashMap;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
12
13use super::types::{GrammarInfo, GrammarRegistry, GrammarSource, GrammarSpec, PackageManifest};
14
15pub trait GrammarLoader: Send + Sync {
22 fn grammars_dir(&self) -> Option<PathBuf>;
24
25 fn languages_packages_dir(&self) -> Option<PathBuf>;
27
28 fn bundles_packages_dir(&self) -> Option<PathBuf>;
30
31 fn read_file(&self, path: &Path) -> io::Result<String>;
33
34 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>>;
36
37 fn exists(&self, path: &Path) -> bool;
39
40 fn is_dir(&self, path: &Path) -> bool;
42}
43
44pub struct LocalGrammarLoader {
46 config_dir: Option<PathBuf>,
47}
48
49impl LocalGrammarLoader {
50 pub fn new(config_dir: PathBuf) -> Self {
52 Self {
53 config_dir: Some(config_dir),
54 }
55 }
56
57 pub fn embedded_only() -> Self {
59 Self { config_dir: None }
60 }
61}
62
63impl GrammarLoader for LocalGrammarLoader {
64 fn grammars_dir(&self) -> Option<PathBuf> {
65 self.config_dir.as_ref().map(|p| p.join("grammars"))
66 }
67
68 fn languages_packages_dir(&self) -> Option<PathBuf> {
69 self.config_dir
70 .as_ref()
71 .map(|p| p.join("languages/packages"))
72 }
73
74 fn bundles_packages_dir(&self) -> Option<PathBuf> {
75 self.config_dir.as_ref().map(|p| p.join("bundles/packages"))
76 }
77
78 fn read_file(&self, path: &Path) -> io::Result<String> {
79 std::fs::read_to_string(path)
80 }
81
82 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
83 let mut entries = Vec::new();
84 for entry in std::fs::read_dir(path)? {
85 entries.push(entry?.path());
86 }
87 Ok(entries)
88 }
89
90 fn exists(&self, path: &Path) -> bool {
91 path.exists()
92 }
93
94 fn is_dir(&self, path: &Path) -> bool {
95 path.is_dir()
96 }
97}
98
99impl GrammarRegistry {
101 pub fn load(loader: &dyn GrammarLoader) -> Self {
109 Self::load_with_additional(loader, &[])
110 }
111
112 pub fn for_editor(config_dir: std::path::PathBuf) -> Arc<Self> {
115 Arc::new(Self::load(&LocalGrammarLoader::new(config_dir)))
116 }
117
118 pub fn for_editor_with_additional(
124 config_dir: std::path::PathBuf,
125 additional: &[GrammarSpec],
126 ) -> Arc<Self> {
127 Arc::new(Self::load_with_additional(
128 &LocalGrammarLoader::new(config_dir),
129 additional,
130 ))
131 }
132
133 pub fn load_with_additional(loader: &dyn GrammarLoader, additional: &[GrammarSpec]) -> Self {
138 let mut user_extensions = Self::build_extra_extensions();
140
141 let has_user_grammars = loader.grammars_dir().is_some_and(|dir| loader.exists(&dir));
143 let has_language_packs = loader
144 .languages_packages_dir()
145 .is_some_and(|dir| loader.exists(&dir));
146 let has_bundle_packs = loader
147 .bundles_packages_dir()
148 .is_some_and(|dir| loader.exists(&dir));
149
150 let needs_builder =
151 has_user_grammars || has_language_packs || has_bundle_packs || !additional.is_empty();
152 let mut loaded_grammar_paths = Vec::new();
153 let mut grammar_sources: HashMap<String, GrammarInfo>;
154
155 let syntax_set = if !needs_builder {
156 tracing::info!(
158 "[grammar-build] No user grammars, language packs, or plugin grammars — using pre-compiled packdump"
159 );
160 let ss: SyntaxSet = syntect::dumps::from_uncompressed_data(include_bytes!(concat!(
161 env!("OUT_DIR"),
162 "/default_syntaxes.packdump"
163 )))
164 .expect("Failed to load pre-compiled syntax packdump");
165 tracing::info!(
166 "[grammar-build] Loaded {} syntaxes from packdump",
167 ss.syntaxes().len()
168 );
169 grammar_sources = Self::build_grammar_sources_from_syntax_set(&ss);
171 ss
172 } else {
173 tracing::info!("[grammar-build] Loading pre-compiled packdump as builder base...");
175 let base: SyntaxSet = syntect::dumps::from_uncompressed_data(include_bytes!(concat!(
176 env!("OUT_DIR"),
177 "/default_syntaxes.packdump"
178 )))
179 .expect("Failed to load pre-compiled syntax packdump");
180 grammar_sources = Self::build_grammar_sources_from_syntax_set(&base);
182 tracing::info!("[grammar-build] Converting to builder...");
183 let mut builder = base.into_builder();
184
185 if has_user_grammars {
186 let grammars_dir = loader.grammars_dir().unwrap();
187 tracing::info!(
188 "[grammar-build] Loading user grammars from {:?}...",
189 grammars_dir
190 );
191 load_user_grammars(
192 loader,
193 &grammars_dir,
194 &mut builder,
195 &mut user_extensions,
196 &mut grammar_sources,
197 );
198 }
199
200 if has_language_packs {
201 let packages_dir = loader.languages_packages_dir().unwrap();
202 tracing::info!(
203 "[grammar-build] Loading language pack grammars from {:?}...",
204 packages_dir
205 );
206 load_language_pack_grammars(
207 loader,
208 &packages_dir,
209 &mut builder,
210 &mut user_extensions,
211 &mut grammar_sources,
212 );
213 }
214
215 if has_bundle_packs {
216 let bundles_dir = loader.bundles_packages_dir().unwrap();
217 tracing::info!(
218 "[grammar-build] Loading bundle grammars from {:?}...",
219 bundles_dir
220 );
221 load_bundle_grammars(
222 loader,
223 &bundles_dir,
224 &mut builder,
225 &mut user_extensions,
226 &mut loaded_grammar_paths,
227 &mut grammar_sources,
228 );
229 }
230
231 if !additional.is_empty() {
233 tracing::info!(
234 "[grammar-build] Adding {} plugin-registered grammars...",
235 additional.len()
236 );
237 for spec in additional {
238 match Self::load_grammar_file(&spec.path) {
239 Ok(syntax) => {
240 let scope = syntax.scope.to_string();
241 let syntax_name = syntax.name.clone();
242 tracing::info!(
243 "[grammar-build] Loaded plugin grammar '{}' from {:?}",
244 spec.language,
245 spec.path
246 );
247 builder.add(syntax);
248 for ext in &spec.extensions {
249 user_extensions.insert(ext.clone(), scope.clone());
250 }
251 grammar_sources.insert(
252 syntax_name.clone(),
253 GrammarInfo {
254 name: syntax_name,
255 source: GrammarSource::Plugin {
256 plugin: spec.language.clone(),
257 path: spec.path.clone(),
258 },
259 file_extensions: spec.extensions.clone(),
260 short_name: None,
261 },
262 );
263 loaded_grammar_paths.push(spec.clone());
264 }
265 Err(e) => {
266 tracing::warn!(
267 "[grammar-build] Failed to load plugin grammar '{}' from {:?}: {}",
268 spec.language,
269 spec.path,
270 e
271 );
272 }
273 }
274 }
275 }
276
277 tracing::info!(
278 "[grammar-build] Building syntax set ({} syntaxes)...",
279 builder.syntaxes().len()
280 );
281 let ss = builder.build();
282 tracing::info!("[grammar-build] Syntax set built");
283 ss
284 };
285 let filename_scopes = Self::build_filename_scopes();
286
287 tracing::info!(
288 "Loaded {} syntaxes, {} user extension mappings, {} filename mappings",
289 syntax_set.syntaxes().len(),
290 user_extensions.len(),
291 filename_scopes.len()
292 );
293
294 let mut registry = Self::new_with_loaded_paths(
295 syntax_set,
296 user_extensions,
297 filename_scopes,
298 loaded_grammar_paths,
299 grammar_sources,
300 );
301
302 registry.populate_built_in_aliases();
304 let manifest_aliases: Vec<(String, String)> = registry
305 .grammar_sources()
306 .values()
307 .filter_map(|info| {
308 info.short_name
309 .as_ref()
310 .map(|short| (short.clone(), info.name.clone()))
311 })
312 .collect();
313 for (short, full) in &manifest_aliases {
314 registry.register_alias(short, full);
315 }
316
317 registry
318 }
319
320 pub fn grammars_directory(config_dir: &std::path::Path) -> PathBuf {
322 config_dir.join("grammars")
323 }
324}
325
326fn load_user_grammars(
328 loader: &dyn GrammarLoader,
329 dir: &Path,
330 builder: &mut SyntaxSetBuilder,
331 user_extensions: &mut HashMap<String, String>,
332 grammar_sources: &mut HashMap<String, GrammarInfo>,
333) {
334 let entries = match loader.read_dir(dir) {
336 Ok(entries) => entries,
337 Err(e) => {
338 tracing::warn!("Failed to read grammars directory {:?}: {}", dir, e);
339 return;
340 }
341 };
342
343 for path in entries {
344 if !loader.is_dir(&path) {
345 continue;
346 }
347
348 let manifest_path = path.join("package.json");
350 if loader.exists(&manifest_path) {
351 if let Ok(manifest) = parse_package_json(loader, &manifest_path) {
352 process_manifest(
353 loader,
354 &path,
355 manifest,
356 builder,
357 user_extensions,
358 grammar_sources,
359 );
360 }
361 continue;
362 }
363
364 let mut found_any = false;
366 load_direct_grammar(loader, &path, builder, &mut found_any, grammar_sources);
367 }
368}
369
370fn parse_package_json(loader: &dyn GrammarLoader, path: &Path) -> Result<PackageManifest, String> {
372 let content = loader
373 .read_file(path)
374 .map_err(|e| format!("Failed to read file: {}", e))?;
375
376 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))
377}
378
379fn process_manifest(
381 loader: &dyn GrammarLoader,
382 package_dir: &Path,
383 manifest: PackageManifest,
384 builder: &mut SyntaxSetBuilder,
385 user_extensions: &mut HashMap<String, String>,
386 grammar_sources: &mut HashMap<String, GrammarInfo>,
387) {
388 let contributes = match manifest.contributes {
389 Some(c) => c,
390 None => return,
391 };
392
393 let mut lang_extensions: HashMap<String, Vec<String>> = HashMap::new();
395 for lang in &contributes.languages {
396 lang_extensions.insert(lang.id.clone(), lang.extensions.clone());
397 }
398
399 for grammar in &contributes.grammars {
401 let grammar_path = package_dir.join(&grammar.path);
402
403 if !loader.exists(&grammar_path) {
404 tracing::warn!("Grammar file not found: {:?}", grammar_path);
405 continue;
406 }
407
408 let grammar_dir = grammar_path.parent().unwrap_or(package_dir);
410 if let Err(e) = builder.add_from_folder(grammar_dir, false) {
411 tracing::warn!("Failed to load grammar {:?}: {}", grammar_path, e);
412 continue;
413 }
414
415 tracing::info!(
416 "Loaded grammar {} from {:?}",
417 grammar.scope_name,
418 grammar_path
419 );
420
421 let extensions: Vec<String> = lang_extensions
423 .get(&grammar.language)
424 .map(|exts| {
425 exts.iter()
426 .map(|ext| {
427 let ext_clean = ext.trim_start_matches('.').to_string();
428 user_extensions.insert(ext_clean.clone(), grammar.scope_name.clone());
429 tracing::debug!(
430 "Mapped extension .{} to {}",
431 ext_clean,
432 grammar.scope_name
433 );
434 ext_clean
435 })
436 .collect()
437 })
438 .unwrap_or_default();
439
440 grammar_sources.insert(
441 grammar.language.clone(),
442 GrammarInfo {
443 name: grammar.language.clone(),
444 source: GrammarSource::User {
445 path: grammar_path.clone(),
446 },
447 file_extensions: extensions,
448 short_name: None,
449 },
450 );
451 }
452}
453
454fn load_direct_grammar(
456 loader: &dyn GrammarLoader,
457 dir: &Path,
458 builder: &mut SyntaxSetBuilder,
459 found_any: &mut bool,
460 grammar_sources: &mut HashMap<String, GrammarInfo>,
461) {
462 let entries = match loader.read_dir(dir) {
464 Ok(e) => e,
465 Err(_) => return,
466 };
467
468 for path in entries {
469 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
470
471 if file_name.ends_with(".tmLanguage") || file_name.ends_with(".sublime-syntax") {
472 let count_before = builder.syntaxes().len();
473 if let Err(e) = builder.add_from_folder(dir, false) {
474 tracing::warn!("Failed to load grammar from {:?}: {}", dir, e);
475 } else {
476 tracing::info!("Loaded grammar from {:?}", dir);
477 *found_any = true;
478 for syntax in builder.syntaxes()[count_before..].iter() {
480 grammar_sources.insert(
481 syntax.name.clone(),
482 GrammarInfo {
483 name: syntax.name.clone(),
484 source: GrammarSource::User {
485 path: dir.to_path_buf(),
486 },
487 file_extensions: syntax.file_extensions.clone(),
488 short_name: None,
489 },
490 );
491 }
492 }
493 break;
494 }
495 }
496}
497
498#[derive(Debug, serde::Deserialize)]
500struct FreshPackageManifest {
501 name: String,
502 #[serde(default)]
503 fresh: Option<FreshConfig>,
504}
505
506#[derive(Debug, serde::Deserialize)]
507struct FreshConfig {
508 #[serde(default)]
509 grammar: Option<FreshGrammarConfig>,
510}
511
512#[derive(Debug, serde::Deserialize)]
513struct FreshGrammarConfig {
514 file: String,
515 #[serde(default)]
516 extensions: Vec<String>,
517 #[serde(default)]
519 short_name: Option<String>,
520}
521
522fn load_language_pack_grammars(
537 loader: &dyn GrammarLoader,
538 packages_dir: &Path,
539 builder: &mut SyntaxSetBuilder,
540 user_extensions: &mut HashMap<String, String>,
541 grammar_sources: &mut HashMap<String, GrammarInfo>,
542) {
543 let entries = match loader.read_dir(packages_dir) {
544 Ok(entries) => entries,
545 Err(e) => {
546 tracing::debug!(
547 "Failed to read language packages directory {:?}: {}",
548 packages_dir,
549 e
550 );
551 return;
552 }
553 };
554
555 for package_path in entries {
556 if !loader.is_dir(&package_path) {
557 continue;
558 }
559
560 let manifest_path = package_path.join("package.json");
561 if !loader.exists(&manifest_path) {
562 continue;
563 }
564
565 let content = match loader.read_file(&manifest_path) {
567 Ok(c) => c,
568 Err(e) => {
569 tracing::debug!("Failed to read {:?}: {}", manifest_path, e);
570 continue;
571 }
572 };
573
574 let manifest: FreshPackageManifest = match serde_json::from_str(&content) {
575 Ok(m) => m,
576 Err(e) => {
577 tracing::debug!("Failed to parse {:?}: {}", manifest_path, e);
578 continue;
579 }
580 };
581
582 let grammar_config = match manifest.fresh.and_then(|f| f.grammar) {
584 Some(g) => g,
585 None => continue,
586 };
587
588 let grammar_path = package_path.join(&grammar_config.file);
589 if !loader.exists(&grammar_path) {
590 tracing::warn!(
591 "Grammar file not found for language pack '{}': {:?}",
592 manifest.name,
593 grammar_path
594 );
595 continue;
596 }
597
598 let content = match loader.read_file(&grammar_path) {
600 Ok(c) => c,
601 Err(e) => {
602 tracing::warn!("Failed to read grammar file {:?}: {}", grammar_path, e);
603 continue;
604 }
605 };
606
607 match syntect::parsing::SyntaxDefinition::load_from_str(
609 &content,
610 true,
611 grammar_path.file_stem().and_then(|s| s.to_str()),
612 ) {
613 Ok(syntax) => {
614 let scope = syntax.scope.to_string();
615 let syntax_name = syntax.name.clone();
616 tracing::info!(
617 "Loaded language pack grammar '{}' from {:?} (scope: {}, extensions: {:?})",
618 manifest.name,
619 grammar_path,
620 scope,
621 grammar_config.extensions
622 );
623 builder.add(syntax);
624
625 let mut clean_extensions = Vec::new();
627 for ext in &grammar_config.extensions {
628 let ext_clean = ext.trim_start_matches('.');
629 user_extensions.insert(ext_clean.to_string(), scope.clone());
630 clean_extensions.push(ext_clean.to_string());
631 }
632
633 grammar_sources.insert(
634 syntax_name.clone(),
635 GrammarInfo {
636 name: syntax_name,
637 source: GrammarSource::LanguagePack {
638 name: manifest.name.clone(),
639 path: grammar_path.clone(),
640 },
641 file_extensions: clean_extensions,
642 short_name: grammar_config.short_name.clone(),
643 },
644 );
645 }
646 Err(e) => {
647 tracing::warn!(
648 "Failed to parse grammar for language pack '{}': {}",
649 manifest.name,
650 e
651 );
652 }
653 }
654 }
655}
656
657fn load_bundle_grammars(
663 loader: &dyn GrammarLoader,
664 bundles_dir: &Path,
665 builder: &mut SyntaxSetBuilder,
666 user_extensions: &mut HashMap<String, String>,
667 loaded_grammar_paths: &mut Vec<GrammarSpec>,
668 grammar_sources: &mut HashMap<String, GrammarInfo>,
669) {
670 let entries = match loader.read_dir(bundles_dir) {
671 Ok(entries) => entries,
672 Err(e) => {
673 tracing::debug!(
674 "Failed to read bundle packages directory {:?}: {}",
675 bundles_dir,
676 e
677 );
678 return;
679 }
680 };
681
682 for package_path in entries {
683 if !loader.is_dir(&package_path) {
684 continue;
685 }
686
687 let manifest_path = package_path.join("package.json");
688 if !loader.exists(&manifest_path) {
689 continue;
690 }
691
692 let content = match loader.read_file(&manifest_path) {
693 Ok(c) => c,
694 Err(e) => {
695 tracing::debug!("Failed to read {:?}: {}", manifest_path, e);
696 continue;
697 }
698 };
699
700 let manifest: crate::services::packages::PackageManifest =
702 match serde_json::from_str(&content) {
703 Ok(m) => m,
704 Err(e) => {
705 tracing::debug!("Failed to parse {:?}: {}", manifest_path, e);
706 continue;
707 }
708 };
709
710 let fresh = match &manifest.fresh {
711 Some(f) => f,
712 None => continue,
713 };
714
715 for lang in &fresh.languages {
716 let grammar_config = match &lang.grammar {
717 Some(g) => g,
718 None => continue,
719 };
720
721 let grammar_path = package_path.join(&grammar_config.file);
722 if !loader.exists(&grammar_path) {
723 tracing::warn!(
724 "Bundle grammar file not found for '{}' in '{}': {:?}",
725 lang.id,
726 manifest.name,
727 grammar_path
728 );
729 continue;
730 }
731
732 let content = match loader.read_file(&grammar_path) {
733 Ok(c) => c,
734 Err(e) => {
735 tracing::warn!("Failed to read bundle grammar {:?}: {}", grammar_path, e);
736 continue;
737 }
738 };
739
740 match syntect::parsing::SyntaxDefinition::load_from_str(
741 &content,
742 true,
743 grammar_path.file_stem().and_then(|s| s.to_str()),
744 ) {
745 Ok(syntax) => {
746 let scope = syntax.scope.to_string();
747 let syntax_name = syntax.name.clone();
748 tracing::info!(
749 "Loaded bundle grammar '{}' from {:?} (scope: {}, extensions: {:?})",
750 lang.id,
751 grammar_path,
752 scope,
753 grammar_config.extensions
754 );
755 builder.add(syntax);
756
757 for ext in &grammar_config.extensions {
758 let ext_clean = ext.trim_start_matches('.');
759 user_extensions.insert(ext_clean.to_string(), scope.clone());
760 }
761
762 grammar_sources.insert(
763 syntax_name.clone(),
764 GrammarInfo {
765 name: syntax_name,
766 source: GrammarSource::Bundle {
767 name: manifest.name.clone(),
768 path: grammar_path.clone(),
769 },
770 file_extensions: grammar_config.extensions.clone(),
771 short_name: None,
772 },
773 );
774
775 loaded_grammar_paths.push(GrammarSpec {
776 language: lang.id.clone(),
777 path: grammar_path,
778 extensions: grammar_config.extensions.clone(),
779 });
780 }
781 Err(e) => {
782 tracing::warn!("Failed to parse bundle grammar for '{}': {}", lang.id, e);
783 }
784 }
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792
793 struct MockGrammarLoader {
795 grammars_dir: Option<PathBuf>,
796 files: HashMap<PathBuf, String>,
797 dirs: HashMap<PathBuf, Vec<PathBuf>>,
798 }
799
800 impl MockGrammarLoader {
801 fn new() -> Self {
802 Self {
803 grammars_dir: None,
804 files: HashMap::new(),
805 dirs: HashMap::new(),
806 }
807 }
808
809 #[allow(dead_code)]
810 fn with_grammars_dir(mut self, dir: PathBuf) -> Self {
811 self.grammars_dir = Some(dir);
812 self
813 }
814 }
815
816 impl GrammarLoader for MockGrammarLoader {
817 fn grammars_dir(&self) -> Option<PathBuf> {
818 self.grammars_dir.clone()
819 }
820
821 fn languages_packages_dir(&self) -> Option<PathBuf> {
822 None }
824
825 fn bundles_packages_dir(&self) -> Option<PathBuf> {
826 None }
828
829 fn read_file(&self, path: &Path) -> io::Result<String> {
830 self.files
831 .get(path)
832 .cloned()
833 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))
834 }
835
836 fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
837 self.dirs
838 .get(path)
839 .cloned()
840 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Directory not found"))
841 }
842
843 fn exists(&self, path: &Path) -> bool {
844 self.files.contains_key(path) || self.dirs.contains_key(path)
845 }
846
847 fn is_dir(&self, path: &Path) -> bool {
848 self.dirs.contains_key(path)
849 }
850 }
851
852 #[test]
853 fn test_mock_loader_no_grammars() {
854 let loader = MockGrammarLoader::new();
855 let registry = GrammarRegistry::load(&loader);
856
857 assert!(!registry.available_syntaxes().is_empty());
859 }
860
861 #[test]
862 fn test_local_loader_grammars_dir() {
863 let temp_dir = tempfile::tempdir().unwrap();
864 let config_dir = temp_dir.path().to_path_buf();
865 let loader = LocalGrammarLoader::new(config_dir.clone());
866 let grammars_dir = loader.grammars_dir();
867
868 assert!(grammars_dir.is_some());
870 let dir = grammars_dir.unwrap();
871 assert_eq!(dir, config_dir.join("grammars"));
872 }
873
874 #[test]
875 fn test_for_editor() {
876 let temp_dir = tempfile::tempdir().unwrap();
877 let config_dir = temp_dir.path().to_path_buf();
878 let registry = GrammarRegistry::for_editor(config_dir);
879 assert!(!registry.available_syntaxes().is_empty());
881 }
882
883 #[test]
884 fn test_find_syntax_with_custom_languages_config() {
885 let temp_dir = tempfile::tempdir().unwrap();
886 let mut registry =
887 Arc::try_unwrap(GrammarRegistry::for_editor(temp_dir.path().to_path_buf()))
888 .ok()
889 .expect("registry should have refcount 1");
890
891 let mut languages = std::collections::HashMap::new();
893 languages.insert(
894 "bash".to_string(),
895 crate::config::LanguageConfig {
896 extensions: vec!["myext".to_string()],
897 filenames: vec!["CUSTOMBUILD".to_string()],
898 grammar: "Bourne Again Shell (bash)".to_string(),
899 comment_prefix: Some("#".to_string()),
900 auto_indent: true,
901 auto_close: None,
902 auto_surround: None,
903 textmate_grammar: None,
904 show_whitespace_tabs: true,
905 line_wrap: None,
906 wrap_column: None,
907 page_view: None,
908 page_width: None,
909 use_tabs: None,
910 tab_size: None,
911 formatter: None,
912 format_on_save: false,
913 on_save: vec![],
914 word_characters: None,
915 indent: None,
916 },
917 );
918 registry.apply_language_config(&languages);
919
920 let entry = registry
922 .find_by_path(Path::new("CUSTOMBUILD"), None)
923 .unwrap();
924 assert!(
925 entry.display_name.to_lowercase().contains("bash")
926 || entry.display_name.to_lowercase().contains("shell"),
927 "CUSTOMBUILD should resolve to shell/bash, got: {}",
928 entry.display_name
929 );
930
931 let entry = registry
933 .find_by_path(Path::new("script.myext"), None)
934 .unwrap();
935 assert!(
936 entry.display_name.to_lowercase().contains("bash")
937 || entry.display_name.to_lowercase().contains("shell"),
938 "script.myext should resolve to shell/bash, got: {}",
939 entry.display_name
940 );
941 }
942
943 #[test]
944 fn test_load_delegates_to_load_with_additional() {
945 let loader = MockGrammarLoader::new();
947 let registry_via_load = GrammarRegistry::load(&loader);
948 let registry_via_additional = GrammarRegistry::load_with_additional(&loader, &[]);
949
950 assert_eq!(
951 registry_via_load.available_syntaxes().len(),
952 registry_via_additional.available_syntaxes().len()
953 );
954 assert_eq!(
955 registry_via_load.user_extensions().len(),
956 registry_via_additional.user_extensions().len()
957 );
958 assert!(registry_via_additional.loaded_grammar_paths().is_empty());
960 }
961
962 #[test]
963 fn test_load_with_additional_empty_is_same_as_load() {
964 let temp_dir = tempfile::tempdir().unwrap();
966 let config_dir = temp_dir.path().to_path_buf();
967 let registry = GrammarRegistry::for_editor_with_additional(config_dir, &[]);
968 assert!(!registry.available_syntaxes().is_empty());
969 assert!(registry.loaded_grammar_paths().is_empty());
970 }
971
972 #[test]
973 fn test_load_with_additional_bad_path_is_skipped() {
974 let loader = MockGrammarLoader::new();
975 let specs = vec![GrammarSpec {
976 language: "nonexistent".to_string(),
977 path: PathBuf::from("/nonexistent/grammar.sublime-syntax"),
978 extensions: vec!["nope".to_string()],
979 }];
980 let registry = GrammarRegistry::load_with_additional(&loader, &specs);
981 assert!(!registry.available_syntaxes().is_empty());
983 assert!(registry.loaded_grammar_paths().is_empty());
985 assert!(!registry.user_extensions().contains_key("nope"));
987 }
988
989 #[test]
990 fn test_list_all_syntaxes() {
991 let temp_dir = tempfile::tempdir().unwrap();
992 let registry = GrammarRegistry::for_editor(temp_dir.path().to_path_buf());
993 let syntax_set = registry.syntax_set();
994
995 let mut syntaxes: Vec<_> = syntax_set
996 .syntaxes()
997 .iter()
998 .map(|s| (s.name.as_str(), s.file_extensions.clone()))
999 .collect();
1000 syntaxes.sort_by(|a, b| a.0.cmp(b.0));
1001
1002 println!("\n=== Available Syntaxes ({} total) ===", syntaxes.len());
1003 for (name, exts) in &syntaxes {
1004 println!(" {} -> {:?}", name, exts);
1005 }
1006
1007 println!("\n=== TypeScript Check ===");
1009 let ts_syntax = syntax_set.find_syntax_by_extension("ts");
1010 let tsx_syntax = syntax_set.find_syntax_by_extension("tsx");
1011 println!(" .ts -> {:?}", ts_syntax.map(|s| &s.name));
1012 println!(" .tsx -> {:?}", tsx_syntax.map(|s| &s.name));
1013
1014 assert!(!syntaxes.is_empty());
1016 }
1017}