1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use syntect::parsing::{SyntaxDefinition, SyntaxReference, SyntaxSet, SyntaxSetBuilder};
11
12pub use crate::primitives::glob_match::{
14 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
15};
16
17#[derive(Clone, Debug)]
22pub struct GrammarSpec {
23 pub language: String,
25 pub path: PathBuf,
27 pub extensions: Vec<String>,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(tag = "type")]
34pub enum GrammarSource {
35 #[serde(rename = "built-in")]
37 BuiltIn,
38 #[serde(rename = "user")]
40 User { path: PathBuf },
41 #[serde(rename = "language-pack")]
43 LanguagePack { name: String, path: PathBuf },
44 #[serde(rename = "bundle")]
46 Bundle { name: String, path: PathBuf },
47 #[serde(rename = "plugin")]
49 Plugin { plugin: String, path: PathBuf },
50}
51
52impl std::fmt::Display for GrammarSource {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 GrammarSource::BuiltIn => write!(f, "built-in"),
56 GrammarSource::User { path } => write!(f, "user ({})", path.display()),
57 GrammarSource::LanguagePack { name, .. } => write!(f, "language-pack ({})", name),
58 GrammarSource::Bundle { name, .. } => write!(f, "bundle ({})", name),
59 GrammarSource::Plugin { plugin, .. } => write!(f, "plugin ({})", plugin),
60 }
61 }
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct GrammarInfo {
67 pub name: String,
69 pub source: GrammarSource,
71 pub file_extensions: Vec<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub short_name: Option<String>,
76}
77
78const SYNTECT_TO_TREE_SITTER_ALIASES: &[(&str, fresh_languages::Language)] =
86 &[("Bourne Again Shell (bash)", fresh_languages::Language::Bash)];
87
88fn tree_sitter_for_syntect_name(display_name: &str) -> Option<fresh_languages::Language> {
91 for (syntect_name, lang) in SYNTECT_TO_TREE_SITTER_ALIASES {
92 if *syntect_name == display_name {
93 return Some(*lang);
94 }
95 }
96 fresh_languages::Language::all()
97 .iter()
98 .find(|l| l.display_name() == display_name)
99 .copied()
100}
101
102#[derive(Clone, Debug, Default)]
107pub struct GrammarEngines {
108 pub syntect: Option<usize>,
111 pub tree_sitter: Option<fresh_languages::Language>,
113}
114
115#[derive(Clone, Debug)]
124pub struct GrammarEntry {
125 pub display_name: String,
127 pub language_id: String,
129 pub short_name: Option<String>,
131 pub extensions: Vec<String>,
133 pub filenames: Vec<String>,
135 pub filename_globs: Vec<String>,
137 pub source: GrammarSource,
139 pub engines: GrammarEngines,
141}
142
143pub const TOML_GRAMMAR: &str = include_str!("../../grammars/toml.sublime-syntax");
145
146pub const ODIN_GRAMMAR: &str = include_str!("../../grammars/odin/Odin.sublime-syntax");
149
150pub const ZIG_GRAMMAR: &str = include_str!("../../grammars/zig.sublime-syntax");
152
153pub const GDSCRIPT_GRAMMAR: &str = include_str!("../../grammars/gdscript.sublime-syntax");
156
157pub const GIT_REBASE_GRAMMAR: &str = include_str!("../../grammars/git-rebase.sublime-syntax");
159
160pub const GIT_COMMIT_GRAMMAR: &str = include_str!("../../grammars/git-commit.sublime-syntax");
162
163pub const GITIGNORE_GRAMMAR: &str = include_str!("../../grammars/gitignore.sublime-syntax");
165
166pub const GITCONFIG_GRAMMAR: &str = include_str!("../../grammars/gitconfig.sublime-syntax");
168
169pub const GITATTRIBUTES_GRAMMAR: &str = include_str!("../../grammars/gitattributes.sublime-syntax");
171
172pub const TYPST_GRAMMAR: &str = include_str!("../../grammars/typst.sublime-syntax");
174
175pub const DOCKERFILE_GRAMMAR: &str = include_str!("../../grammars/dockerfile.sublime-syntax");
177pub const INI_GRAMMAR: &str = include_str!("../../grammars/ini.sublime-syntax");
179pub const CMAKE_GRAMMAR: &str = include_str!("../../grammars/cmake.sublime-syntax");
181pub const SCSS_GRAMMAR: &str = include_str!("../../grammars/scss.sublime-syntax");
183pub const LESS_GRAMMAR: &str = include_str!("../../grammars/less.sublime-syntax");
185pub const POWERSHELL_GRAMMAR: &str = include_str!("../../grammars/powershell.sublime-syntax");
187pub const KOTLIN_GRAMMAR: &str = include_str!("../../grammars/kotlin.sublime-syntax");
189pub const SWIFT_GRAMMAR: &str = include_str!("../../grammars/swift.sublime-syntax");
191pub const DART_GRAMMAR: &str = include_str!("../../grammars/dart.sublime-syntax");
193pub const ELIXIR_GRAMMAR: &str = include_str!("../../grammars/elixir.sublime-syntax");
195pub const FSHARP_GRAMMAR: &str = include_str!("../../grammars/fsharp.sublime-syntax");
197pub const NIX_GRAMMAR: &str = include_str!("../../grammars/nix.sublime-syntax");
199pub const HCL_GRAMMAR: &str = include_str!("../../grammars/hcl.sublime-syntax");
201pub const PROTOBUF_GRAMMAR: &str = include_str!("../../grammars/protobuf.sublime-syntax");
203pub const GRAPHQL_GRAMMAR: &str = include_str!("../../grammars/graphql.sublime-syntax");
205pub const JULIA_GRAMMAR: &str = include_str!("../../grammars/julia.sublime-syntax");
207pub const NIM_GRAMMAR: &str = include_str!("../../grammars/nim.sublime-syntax");
209pub const GLEAM_GRAMMAR: &str = include_str!("../../grammars/gleam.sublime-syntax");
211pub const VLANG_GRAMMAR: &str = include_str!("../../grammars/vlang.sublime-syntax");
213pub const SOLIDITY_GRAMMAR: &str = include_str!("../../grammars/solidity.sublime-syntax");
215pub const KDL_GRAMMAR: &str = include_str!("../../grammars/kdl.sublime-syntax");
217pub const NUSHELL_GRAMMAR: &str = include_str!("../../grammars/nushell.sublime-syntax");
219pub const SMALI_GRAMMAR: &str = include_str!("../../grammars/smali.sublime-syntax");
221pub const FISH_GRAMMAR: &str = include_str!("../../grammars/fish.sublime-syntax");
223pub const STARLARK_GRAMMAR: &str = include_str!("../../grammars/starlark.sublime-syntax");
225pub const JUSTFILE_GRAMMAR: &str = include_str!("../../grammars/justfile.sublime-syntax");
227pub const EARTHFILE_GRAMMAR: &str = include_str!("../../grammars/earthfile.sublime-syntax");
229pub const GOMOD_GRAMMAR: &str = include_str!("../../grammars/gomod.sublime-syntax");
231pub const VUE_GRAMMAR: &str = include_str!("../../grammars/vue.sublime-syntax");
233pub const SVELTE_GRAMMAR: &str = include_str!("../../grammars/svelte.sublime-syntax");
235pub const ASTRO_GRAMMAR: &str = include_str!("../../grammars/astro.sublime-syntax");
237pub const HYPRLANG_GRAMMAR: &str = include_str!("../../grammars/hyprlang.sublime-syntax");
239pub const AUTOHOTKEY_GRAMMAR: &str =
242 include_str!("../../grammars/autohotkey/AutoHotkey.sublime-syntax");
243pub const RACKET_GRAMMAR: &str = include_str!("../../grammars/racket.sublime-syntax");
245pub const VERILOG_GRAMMAR: &str = include_str!("../../grammars/verilog.sublime-syntax");
247pub const SYSTEMVERILOG_GRAMMAR: &str = include_str!("../../grammars/systemverilog.sublime-syntax");
249pub const VHDL_GRAMMAR: &str = include_str!("../../grammars/vhdl.sublime-syntax");
251
252pub const C3_GRAMMAR: &str = include_str!("../../grammars/c3.sublime-syntax");
253
254pub const ASM_GRAMMAR: &str = include_str!("../../grammars/asm.sublime-syntax");
257
258impl std::fmt::Debug for GrammarRegistry {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 f.debug_struct("GrammarRegistry")
265 .field("syntax_count", &self.syntax_set.syntaxes().len())
266 .finish()
267 }
268}
269
270pub struct GrammarRegistry {
271 syntax_set: Arc<SyntaxSet>,
273 user_extensions: HashMap<String, String>,
275 filename_scopes: HashMap<String, String>,
277 loaded_grammar_paths: Vec<GrammarSpec>,
279 grammar_sources: HashMap<String, GrammarInfo>,
281 aliases: HashMap<String, String>,
285 catalog: Vec<GrammarEntry>,
289 catalog_by_name: HashMap<String, usize>,
292 catalog_by_extension: HashMap<String, usize>,
294 catalog_by_filename: HashMap<String, usize>,
296 applied_language_config: HashMap<String, crate::config::LanguageConfig>,
301 catalog_gen: u64,
305}
306
307impl GrammarRegistry {
308 pub(crate) fn new(
313 syntax_set: SyntaxSet,
314 user_extensions: HashMap<String, String>,
315 filename_scopes: HashMap<String, String>,
316 ) -> Self {
317 Self::new_with_loaded_paths(
318 syntax_set,
319 user_extensions,
320 filename_scopes,
321 Vec::new(),
322 HashMap::new(),
323 )
324 }
325
326 pub(crate) fn new_with_loaded_paths(
331 syntax_set: SyntaxSet,
332 user_extensions: HashMap<String, String>,
333 filename_scopes: HashMap<String, String>,
334 loaded_grammar_paths: Vec<GrammarSpec>,
335 grammar_sources: HashMap<String, GrammarInfo>,
336 ) -> Self {
337 let mut reg = Self {
338 syntax_set: Arc::new(syntax_set),
339 user_extensions,
340 filename_scopes,
341 loaded_grammar_paths,
342 grammar_sources,
343 aliases: HashMap::new(),
344 catalog: Vec::new(),
345 catalog_by_name: HashMap::new(),
346 catalog_by_extension: HashMap::new(),
347 catalog_by_filename: HashMap::new(),
348 applied_language_config: HashMap::new(),
349 catalog_gen: 0,
350 };
351 reg.rebuild_catalog();
352 reg
353 }
354
355 pub fn empty() -> Arc<Self> {
357 let mut builder = SyntaxSetBuilder::new();
358 builder.add_plain_text_syntax();
359 let mut reg = Self {
360 syntax_set: Arc::new(builder.build()),
361 user_extensions: HashMap::new(),
362 filename_scopes: HashMap::new(),
363 loaded_grammar_paths: Vec::new(),
364 grammar_sources: HashMap::new(),
365 aliases: HashMap::new(),
366 catalog: Vec::new(),
367 catalog_by_name: HashMap::new(),
368 catalog_by_extension: HashMap::new(),
369 catalog_by_filename: HashMap::new(),
370 applied_language_config: HashMap::new(),
371 catalog_gen: 0,
372 };
373 reg.rebuild_catalog();
374 Arc::new(reg)
375 }
376
377 pub fn defaults_only() -> Arc<Self> {
384 tracing::info!("defaults_only: loading pre-compiled syntax packdump...");
388 let syntax_set: SyntaxSet = syntect::dumps::from_uncompressed_data(include_bytes!(
389 concat!(env!("OUT_DIR"), "/default_syntaxes.packdump")
390 ))
391 .expect("Failed to load pre-compiled syntax packdump");
392 tracing::info!(
393 "defaults_only: loaded ({} syntaxes)",
394 syntax_set.syntaxes().len()
395 );
396 let grammar_sources = Self::build_grammar_sources_from_syntax_set(&syntax_set);
397 let filename_scopes = Self::build_filename_scopes();
398 let extra_extensions = Self::build_extra_extensions();
399 let mut registry = Self {
400 syntax_set: Arc::new(syntax_set),
401 user_extensions: extra_extensions,
402 filename_scopes,
403 loaded_grammar_paths: Vec::new(),
404 grammar_sources,
405 aliases: HashMap::new(),
406 catalog: Vec::new(),
407 catalog_by_name: HashMap::new(),
408 catalog_by_extension: HashMap::new(),
409 catalog_by_filename: HashMap::new(),
410 applied_language_config: HashMap::new(),
411 catalog_gen: 0,
412 };
413 registry.populate_built_in_aliases();
414 registry.rebuild_catalog();
415 Arc::new(registry)
416 }
417
418 pub(crate) fn build_extra_extensions() -> HashMap<String, String> {
423 let mut map = HashMap::new();
424
425 let js_scope = "source.js".to_string();
427 map.insert("cjs".to_string(), js_scope.clone());
428 map.insert("mjs".to_string(), js_scope);
429
430 map
434 }
435
436 pub(crate) fn build_filename_scopes() -> HashMap<String, String> {
438 let mut map = HashMap::new();
439
440 let shell_scope = "source.shell.bash".to_string();
442 for filename in [
443 ".zshrc",
444 ".zprofile",
445 ".zshenv",
446 ".zlogin",
447 ".zlogout",
448 ".bash_aliases",
449 "PKGBUILD",
452 "APKBUILD",
453 ] {
454 map.insert(filename.to_string(), shell_scope.clone());
455 }
456
457 let git_rebase_scope = "source.git-rebase-todo".to_string();
459 map.insert("git-rebase-todo".to_string(), git_rebase_scope);
460
461 let git_commit_scope = "source.git-commit".to_string();
463 for filename in ["COMMIT_EDITMSG", "MERGE_MSG", "SQUASH_MSG", "TAG_EDITMSG"] {
464 map.insert(filename.to_string(), git_commit_scope.clone());
465 }
466
467 let gitignore_scope = "source.gitignore".to_string();
469 for filename in [".gitignore", ".dockerignore", ".npmignore", ".hgignore"] {
470 map.insert(filename.to_string(), gitignore_scope.clone());
471 }
472
473 let gitconfig_scope = "source.gitconfig".to_string();
475 for filename in [".gitconfig", ".gitmodules"] {
476 map.insert(filename.to_string(), gitconfig_scope.clone());
477 }
478
479 let gitattributes_scope = "source.gitattributes".to_string();
481 map.insert(".gitattributes".to_string(), gitattributes_scope);
482
483 let groovy_scope = "source.groovy".to_string();
485 map.insert("Jenkinsfile".to_string(), groovy_scope);
486
487 let ruby_scope = "source.ruby".to_string();
490 map.insert("Brewfile".to_string(), ruby_scope);
491
492 let dockerfile_scope = "source.dockerfile".to_string();
494 map.insert("Dockerfile".to_string(), dockerfile_scope.clone());
495 map.insert("Containerfile".to_string(), dockerfile_scope.clone());
496 map.insert("Dockerfile.dev".to_string(), dockerfile_scope.clone());
498 map.insert("Dockerfile.prod".to_string(), dockerfile_scope.clone());
499 map.insert("Dockerfile.test".to_string(), dockerfile_scope.clone());
500 map.insert("Dockerfile.build".to_string(), dockerfile_scope.clone());
501
502 let cmake_scope = "source.cmake".to_string();
504 map.insert("CMakeLists.txt".to_string(), cmake_scope);
505
506 let starlark_scope = "source.starlark".to_string();
508 map.insert("BUILD".to_string(), starlark_scope.clone());
509 map.insert("BUILD.bazel".to_string(), starlark_scope.clone());
510 map.insert("WORKSPACE".to_string(), starlark_scope.clone());
511 map.insert("WORKSPACE.bazel".to_string(), starlark_scope.clone());
512 map.insert("Tiltfile".to_string(), starlark_scope);
513
514 let justfile_scope = "source.justfile".to_string();
516 map.insert("justfile".to_string(), justfile_scope.clone());
517 map.insert("Justfile".to_string(), justfile_scope.clone());
518 map.insert(".justfile".to_string(), justfile_scope);
519
520 let ini_scope = "source.ini".to_string();
522 map.insert(".editorconfig".to_string(), ini_scope);
523
524 let earthfile_scope = "source.earthfile".to_string();
526 map.insert("Earthfile".to_string(), earthfile_scope);
527
528 let hyprlang_scope = "source.hyprlang".to_string();
530 map.insert("hyprland.conf".to_string(), hyprlang_scope.clone());
531 map.insert("hyprpaper.conf".to_string(), hyprlang_scope.clone());
532 map.insert("hyprlock.conf".to_string(), hyprlang_scope);
533
534 let gomod_scope = "source.gomod".to_string();
536 map.insert("go.mod".to_string(), gomod_scope.clone());
537 map.insert("go.sum".to_string(), gomod_scope);
538
539 let yaml_scope = "source.yaml".to_string();
546 for filename in [
547 "yarn.lock",
548 ".clang-format",
549 "_clang-format",
550 ".clang-tidy",
551 ".yamllint",
552 "Podfile.lock",
553 "pubspec.lock",
554 ] {
555 map.insert(filename.to_string(), yaml_scope.clone());
556 }
557
558 let toml_scope = "source.toml".to_string();
562 for filename in ["Cargo.lock", "poetry.lock", "uv.lock"] {
563 map.insert(filename.to_string(), toml_scope.clone());
564 }
565
566 let json_scope = "source.json".to_string();
569 for filename in ["composer.lock", "Pipfile.lock", "flake.lock", "deno.lock"] {
570 map.insert(filename.to_string(), json_scope.clone());
571 }
572
573 map
574 }
575
576 pub(crate) fn add_embedded_grammars(builder: &mut SyntaxSetBuilder) {
578 match SyntaxDefinition::load_from_str(TOML_GRAMMAR, true, Some("TOML")) {
580 Ok(syntax) => {
581 builder.add(syntax);
582 tracing::debug!("Loaded embedded TOML grammar");
583 }
584 Err(e) => {
585 tracing::warn!("Failed to load embedded TOML grammar: {}", e);
586 }
587 }
588
589 match SyntaxDefinition::load_from_str(ODIN_GRAMMAR, true, Some("Odin")) {
591 Ok(syntax) => {
592 builder.add(syntax);
593 tracing::debug!("Loaded embedded Odin grammar");
594 }
595 Err(e) => {
596 tracing::warn!("Failed to load embedded Odin grammar: {}", e);
597 }
598 }
599
600 match SyntaxDefinition::load_from_str(ZIG_GRAMMAR, true, Some("Zig")) {
602 Ok(syntax) => {
603 builder.add(syntax);
604 tracing::debug!("Loaded embedded Zig grammar");
605 }
606 Err(e) => {
607 tracing::warn!("Failed to load embedded Zig grammar: {}", e);
608 }
609 }
610
611 match SyntaxDefinition::load_from_str(GDSCRIPT_GRAMMAR, true, Some("GDScript")) {
613 Ok(syntax) => {
614 builder.add(syntax);
615 tracing::debug!("Loaded embedded GDScript grammar");
616 }
617 Err(e) => {
618 tracing::warn!("Failed to load embedded GDScript grammar: {}", e);
619 }
620 }
621
622 match SyntaxDefinition::load_from_str(GIT_REBASE_GRAMMAR, true, Some("Git Rebase Todo")) {
624 Ok(syntax) => {
625 builder.add(syntax);
626 tracing::debug!("Loaded embedded Git Rebase Todo grammar");
627 }
628 Err(e) => {
629 tracing::warn!("Failed to load embedded Git Rebase Todo grammar: {}", e);
630 }
631 }
632
633 match SyntaxDefinition::load_from_str(GIT_COMMIT_GRAMMAR, true, Some("Git Commit Message"))
635 {
636 Ok(syntax) => {
637 builder.add(syntax);
638 tracing::debug!("Loaded embedded Git Commit Message grammar");
639 }
640 Err(e) => {
641 tracing::warn!("Failed to load embedded Git Commit Message grammar: {}", e);
642 }
643 }
644
645 match SyntaxDefinition::load_from_str(GITIGNORE_GRAMMAR, true, Some("Gitignore")) {
647 Ok(syntax) => {
648 builder.add(syntax);
649 tracing::debug!("Loaded embedded Gitignore grammar");
650 }
651 Err(e) => {
652 tracing::warn!("Failed to load embedded Gitignore grammar: {}", e);
653 }
654 }
655
656 match SyntaxDefinition::load_from_str(GITCONFIG_GRAMMAR, true, Some("Git Config")) {
658 Ok(syntax) => {
659 builder.add(syntax);
660 tracing::debug!("Loaded embedded Git Config grammar");
661 }
662 Err(e) => {
663 tracing::warn!("Failed to load embedded Git Config grammar: {}", e);
664 }
665 }
666
667 match SyntaxDefinition::load_from_str(GITATTRIBUTES_GRAMMAR, true, Some("Git Attributes")) {
669 Ok(syntax) => {
670 builder.add(syntax);
671 tracing::debug!("Loaded embedded Git Attributes grammar");
672 }
673 Err(e) => {
674 tracing::warn!("Failed to load embedded Git Attributes grammar: {}", e);
675 }
676 }
677
678 match SyntaxDefinition::load_from_str(TYPST_GRAMMAR, true, Some("Typst")) {
680 Ok(syntax) => {
681 builder.add(syntax);
682 tracing::debug!("Loaded embedded Typst grammar");
683 }
684 Err(e) => {
685 tracing::warn!("Failed to load embedded Typst grammar: {}", e);
686 }
687 }
688
689 let additional_grammars: &[(&str, &str)] = &[
691 (DOCKERFILE_GRAMMAR, "Dockerfile"),
692 (INI_GRAMMAR, "INI"),
693 (CMAKE_GRAMMAR, "CMake"),
694 (SCSS_GRAMMAR, "SCSS"),
695 (LESS_GRAMMAR, "LESS"),
696 (POWERSHELL_GRAMMAR, "PowerShell"),
697 (KOTLIN_GRAMMAR, "Kotlin"),
698 (SWIFT_GRAMMAR, "Swift"),
699 (DART_GRAMMAR, "Dart"),
700 (ELIXIR_GRAMMAR, "Elixir"),
701 (FSHARP_GRAMMAR, "FSharp"),
702 (NIX_GRAMMAR, "Nix"),
703 (HCL_GRAMMAR, "HCL"),
704 (PROTOBUF_GRAMMAR, "Protocol Buffers"),
705 (GRAPHQL_GRAMMAR, "GraphQL"),
706 (JULIA_GRAMMAR, "Julia"),
707 (NIM_GRAMMAR, "Nim"),
708 (GLEAM_GRAMMAR, "Gleam"),
709 (VLANG_GRAMMAR, "V"),
710 (SOLIDITY_GRAMMAR, "Solidity"),
711 (KDL_GRAMMAR, "KDL"),
712 (NUSHELL_GRAMMAR, "Nushell"),
713 (SMALI_GRAMMAR, "Smali"),
714 (FISH_GRAMMAR, "Fish"),
715 (STARLARK_GRAMMAR, "Starlark"),
716 (JUSTFILE_GRAMMAR, "Justfile"),
717 (EARTHFILE_GRAMMAR, "Earthfile"),
718 (GOMOD_GRAMMAR, "Go Module"),
719 (VUE_GRAMMAR, "Vue"),
720 (SVELTE_GRAMMAR, "Svelte"),
721 (ASTRO_GRAMMAR, "Astro"),
722 (HYPRLANG_GRAMMAR, "Hyprlang"),
723 (AUTOHOTKEY_GRAMMAR, "AutoHotkey"),
724 (RACKET_GRAMMAR, "Racket"),
725 (VERILOG_GRAMMAR, "Verilog"),
726 (SYSTEMVERILOG_GRAMMAR, "SystemVerilog"),
727 (VHDL_GRAMMAR, "VHDL"),
728 (C3_GRAMMAR, "C3"),
729 (ASM_GRAMMAR, "Assembly"),
730 ];
731
732 for (grammar_str, name) in additional_grammars {
733 match SyntaxDefinition::load_from_str(grammar_str, true, Some(name)) {
734 Ok(syntax) => {
735 builder.add(syntax);
736 tracing::debug!("Loaded embedded {} grammar", name);
737 }
738 Err(e) => {
739 tracing::warn!("Failed to load embedded {} grammar: {}", name, e);
740 }
741 }
742 }
743 }
744
745 pub fn find_syntax_for_file(&self, path: &Path) -> Option<&SyntaxReference> {
751 let entry = self.find_by_path(path, None)?;
752 entry
753 .engines
754 .syntect
755 .map(|i| &self.syntax_set.syntaxes()[i])
756 }
757
758 pub fn find_syntax_by_name(&self, name: &str) -> Option<&SyntaxReference> {
766 if let Some(entry) = self.find_by_name(name) {
767 if let Some(idx) = entry.engines.syntect {
768 return Some(&self.syntax_set.syntaxes()[idx]);
769 }
770 }
771 self.syntax_set.find_syntax_by_name(name)
775 }
776
777 fn built_in_aliases() -> Vec<(&'static str, &'static str)> {
786 vec![
787 ("bash", "Bourne Again Shell (bash)"),
789 ("shell", "Bourne Again Shell (bash)"),
790 ("sh", "Bourne Again Shell (bash)"),
791 ("c++", "C++"),
792 ("cpp", "C++"),
793 ("csharp", "C#"),
794 ("objc", "Objective-C"),
795 ("objcpp", "Objective-C++"),
796 ("regex", "Regular Expressions (Python)"),
797 ("regexp", "Regular Expressions (Python)"),
798 ("proto", "Protocol Buffers"),
800 ("protobuf", "Protocol Buffers"),
801 ("gomod", "Go Module"),
802 ("git-rebase", "Git Rebase Todo"),
803 ("git-commit", "Git Commit Message"),
804 ("git-config", "Git Config"),
805 ("git-attributes", "Git Attributes"),
806 ("gitignore", "Gitignore"),
807 ("fsharp", "FSharp"),
808 ("f#", "FSharp"),
809 ("terraform", "HCL"),
810 ("tf", "HCL"),
811 ("ts", "TypeScript"),
812 ("js", "JavaScript"),
813 ("py", "Python"),
814 ("rb", "Ruby"),
815 ("rs", "Rust"),
816 ("md", "Markdown"),
817 ("yml", "YAML"),
818 ("dockerfile", "Dockerfile"),
819 ]
820 }
821
822 pub(crate) fn populate_built_in_aliases(&mut self) {
829 for (short, full) in Self::built_in_aliases() {
830 self.register_alias_inner(short, full, true);
831 }
832 self.rebuild_catalog();
833 }
834
835 pub(crate) fn register_alias(&mut self, short_name: &str, full_name: &str) -> bool {
845 if !self.register_alias_inner(short_name, full_name, false) {
846 return false;
847 }
848 let short_lower = short_name.to_lowercase();
849 let full_lower = full_name.to_lowercase();
850 if let Some(&idx) = self.catalog_by_name.get(&full_lower) {
851 self.catalog_by_name
852 .entry(short_lower.clone())
853 .or_insert(idx);
854 let entry = &mut self.catalog[idx];
855 let replace = match &entry.short_name {
856 None => true,
857 Some(existing) => short_name.len() < existing.len(),
858 };
859 if replace {
860 entry.short_name = Some(short_lower);
861 }
862 }
863 true
864 }
865
866 fn register_alias_inner(
867 &mut self,
868 short_name: &str,
869 full_name: &str,
870 is_built_in: bool,
871 ) -> bool {
872 let short_lower = short_name.to_lowercase();
873
874 let target_exists = self
876 .syntax_set
877 .syntaxes()
878 .iter()
879 .any(|s| s.name.eq_ignore_ascii_case(full_name));
880 if !target_exists {
881 if tree_sitter_for_syntect_name(full_name).is_some() {
885 return false;
886 }
887 if is_built_in {
888 tracing::warn!(
891 "[grammar-alias] Built-in alias '{}' -> '{}': target grammar not found, skipping",
892 short_name, full_name
893 );
894 } else {
895 tracing::warn!(
896 "[grammar-alias] Alias '{}' -> '{}': target grammar not found, skipping",
897 short_name,
898 full_name
899 );
900 }
901 return false;
902 }
903
904 let collides_with_full_name = self
906 .syntax_set
907 .syntaxes()
908 .iter()
909 .any(|s| s.name.eq_ignore_ascii_case(&short_lower));
910 if collides_with_full_name {
911 tracing::debug!(
915 "[grammar-alias] Alias '{}' matches an existing grammar name, skipping (not needed)",
916 short_name
917 );
918 return false;
919 }
920
921 if let Some(existing_target) = self.aliases.get(&short_lower) {
923 if existing_target.eq_ignore_ascii_case(full_name) {
924 return true;
926 }
927 let msg = format!(
928 "Alias '{}' already maps to '{}', cannot remap to '{}'",
929 short_name, existing_target, full_name
930 );
931 if is_built_in {
932 panic!("[grammar-alias] Built-in alias collision: {}", msg);
933 } else {
934 tracing::warn!("[grammar-alias] {}", msg);
935 return false;
936 }
937 }
938
939 let exact_name = self
941 .syntax_set
942 .syntaxes()
943 .iter()
944 .find(|s| s.name.eq_ignore_ascii_case(full_name))
945 .map(|s| s.name.clone())
946 .unwrap();
947
948 self.aliases.insert(short_lower, exact_name);
949 true
950 }
951
952 pub(crate) fn rebuild_catalog(&mut self) {
967 let mut short_by_full: HashMap<String, String> = HashMap::new();
974 let record = |map: &mut HashMap<String, String>, short: &str, full: &str| {
975 let key = full.to_lowercase();
976 let keep = match map.get(&key) {
977 None => true,
978 Some(existing) => short.len() < existing.len(),
979 };
980 if keep {
981 map.insert(key, short.to_string());
982 }
983 };
984 for (short, full) in Self::built_in_aliases() {
985 record(&mut short_by_full, short, full);
986 }
987 for (short, full) in &self.aliases {
988 record(&mut short_by_full, short, full);
989 }
990
991 let derive_language_id =
992 |display_name: &str| -> (String, Option<fresh_languages::Language>) {
993 let ts = tree_sitter_for_syntect_name(display_name);
994 let id = ts
995 .map(|l| l.id().to_string())
996 .unwrap_or_else(|| display_name.to_lowercase());
997 (id, ts)
998 };
999
1000 let mut catalog: Vec<GrammarEntry> = Vec::new();
1001 let mut scope_to_index: HashMap<String, usize> = HashMap::new();
1002
1003 for (idx, syntax) in self.syntax_set.syntaxes().iter().enumerate() {
1024 if syntax.name == "Plain Text" || syntax.name == "JavaScript" {
1025 continue;
1026 }
1027 let (language_id, tree_sitter) = derive_language_id(&syntax.name);
1028 let short_name = short_by_full.get(&syntax.name.to_lowercase()).cloned();
1029 let source = self
1030 .grammar_sources
1031 .get(&syntax.name)
1032 .map(|info| info.source.clone())
1033 .unwrap_or(GrammarSource::BuiltIn);
1034 let entry_index = catalog.len();
1035 scope_to_index.insert(syntax.scope.to_string(), entry_index);
1036
1037 let mut extensions = syntax.file_extensions.clone();
1043 if let Some(lang) = tree_sitter {
1044 for ext in lang.extensions() {
1045 let ext = ext.to_string();
1046 if !extensions.iter().any(|e| e == &ext) {
1047 extensions.push(ext);
1048 }
1049 }
1050 }
1051
1052 if syntax.name != "Fish" {
1057 extensions.retain(|e| e != "fish");
1058 }
1059
1060 catalog.push(GrammarEntry {
1061 display_name: syntax.name.clone(),
1062 language_id,
1063 short_name,
1064 extensions,
1065 filenames: Vec::new(),
1066 filename_globs: Vec::new(),
1067 source,
1068 engines: GrammarEngines {
1069 syntect: Some(idx),
1070 tree_sitter,
1071 },
1072 });
1073 }
1074
1075 for (filename, scope) in &self.filename_scopes {
1077 if let Some(&idx) = scope_to_index.get(scope) {
1078 if !catalog[idx].filenames.iter().any(|f| f == filename) {
1079 catalog[idx].filenames.push(filename.clone());
1080 }
1081 }
1082 }
1083
1084 for (ext, scope) in &self.user_extensions {
1086 if let Some(&idx) = scope_to_index.get(scope) {
1087 if !catalog[idx].extensions.iter().any(|e| e == ext) {
1088 catalog[idx].extensions.push(ext.clone());
1089 }
1090 }
1091 }
1092
1093 let mut ts_covered: std::collections::HashSet<fresh_languages::Language> =
1098 std::collections::HashSet::new();
1099 for entry in &catalog {
1100 if let Some(lang) = entry.engines.tree_sitter {
1101 ts_covered.insert(lang);
1102 }
1103 }
1104 for lang in fresh_languages::Language::all() {
1105 if ts_covered.contains(lang) {
1106 continue;
1107 }
1108 let display_name = lang.display_name().to_string();
1109 let language_id = lang.id().to_string();
1110 let short_name = short_by_full.get(&display_name.to_lowercase()).cloned();
1111 let extensions: Vec<String> = lang.extensions().iter().map(|s| s.to_string()).collect();
1112 catalog.push(GrammarEntry {
1113 display_name,
1114 language_id,
1115 short_name,
1116 extensions,
1117 filenames: Vec::new(),
1118 filename_globs: Vec::new(),
1119 source: GrammarSource::BuiltIn,
1120 engines: GrammarEngines {
1121 syntect: None,
1122 tree_sitter: Some(*lang),
1123 },
1124 });
1125 }
1126
1127 let mut by_name: HashMap<String, usize> = HashMap::new();
1135 let mut by_extension: HashMap<String, usize> = HashMap::new();
1136 let mut by_filename: HashMap<String, usize> = HashMap::new();
1137 for (idx, entry) in catalog.iter().enumerate() {
1138 by_name.insert(entry.display_name.to_lowercase(), idx);
1139 by_name.insert(entry.language_id.to_lowercase(), idx);
1140 if let Some(short) = &entry.short_name {
1141 by_name.insert(short.to_lowercase(), idx);
1142 }
1143 for ext in &entry.extensions {
1144 by_extension.entry(ext.to_lowercase()).or_insert(idx);
1145 by_filename.entry(ext.clone()).or_insert(idx);
1146 }
1147 for filename in &entry.filenames {
1148 by_filename.entry(filename.clone()).or_insert(idx);
1149 }
1150 }
1151
1152 self.catalog = catalog;
1153 self.catalog_by_name = by_name;
1154 self.catalog_by_extension = by_extension;
1155 self.catalog_by_filename = by_filename;
1156
1157 if !self.applied_language_config.is_empty() {
1161 let cfg = std::mem::take(&mut self.applied_language_config);
1162 self.apply_language_config_inner(&cfg);
1163 self.applied_language_config = cfg;
1164 }
1165 self.catalog_gen = self.catalog_gen.wrapping_add(1);
1166 }
1167
1168 pub fn catalog(&self) -> &[GrammarEntry] {
1170 &self.catalog
1171 }
1172
1173 pub fn catalog_gen(&self) -> u64 {
1177 self.catalog_gen
1178 }
1179
1180 pub fn find_by_name(&self, name: &str) -> Option<&GrammarEntry> {
1186 self.catalog_by_name
1187 .get(&name.to_lowercase())
1188 .map(|&idx| &self.catalog[idx])
1189 }
1190
1191 pub fn find_by_path(&self, path: &Path, first_line: Option<&str>) -> Option<&GrammarEntry> {
1212 let filename = path.file_name().and_then(|n| n.to_str());
1213 let path_str = path.to_str().unwrap_or("");
1214
1215 if let Some(name) = filename {
1216 if let Some(&idx) = self.catalog_by_filename.get(name) {
1217 return Some(&self.catalog[idx]);
1218 }
1219 }
1220
1221 if let Some(name) = filename {
1223 for entry in &self.catalog {
1224 for pattern in &entry.filename_globs {
1225 let matched = if is_path_pattern(pattern) {
1226 path_glob_matches(pattern, path_str)
1227 } else {
1228 filename_glob_matches(pattern, name)
1229 };
1230 if matched {
1231 return Some(entry);
1232 }
1233 }
1234 }
1235 }
1236
1237 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1238 if let Some(entry) = self.find_by_extension(ext) {
1239 return Some(entry);
1240 }
1241 }
1242
1243 let line = first_line?;
1248 let syntax = self.syntax_set.find_syntax_by_first_line(line)?;
1249 self.find_by_name(&syntax.name)
1250 }
1251
1252 pub fn find_by_extension(&self, ext: &str) -> Option<&GrammarEntry> {
1254 self.catalog_by_extension
1255 .get(&ext.to_lowercase())
1256 .map(|&idx| &self.catalog[idx])
1257 }
1258
1259 pub fn apply_language_config(
1272 &mut self,
1273 languages: &HashMap<String, crate::config::LanguageConfig>,
1274 ) {
1275 self.applied_language_config = languages.clone();
1276 self.apply_language_config_inner(languages);
1277 self.catalog_gen = self.catalog_gen.wrapping_add(1);
1278 }
1279
1280 fn apply_language_config_inner(
1285 &mut self,
1286 languages: &HashMap<String, crate::config::LanguageConfig>,
1287 ) {
1288 for (lang_id, lang_cfg) in languages {
1289 let grammar_name = if lang_cfg.grammar.is_empty() {
1290 lang_id.as_str()
1291 } else {
1292 lang_cfg.grammar.as_str()
1293 };
1294
1295 let idx = self
1297 .catalog_by_name
1298 .get(&grammar_name.to_lowercase())
1299 .copied()
1300 .or_else(|| self.catalog_by_name.get(&lang_id.to_lowercase()).copied())
1301 .unwrap_or_else(|| {
1302 let idx = self.catalog.len();
1303 self.catalog.push(GrammarEntry {
1304 display_name: lang_id.clone(),
1305 language_id: lang_id.clone(),
1306 short_name: None,
1307 extensions: Vec::new(),
1308 filenames: Vec::new(),
1309 filename_globs: Vec::new(),
1310 source: GrammarSource::BuiltIn,
1311 engines: GrammarEngines::default(),
1312 });
1313 idx
1314 });
1315
1316 self.catalog_by_name
1321 .entry(lang_id.to_lowercase())
1322 .or_insert(idx);
1323
1324 for ext in &lang_cfg.extensions {
1325 if !self.catalog[idx].extensions.iter().any(|e| e == ext) {
1326 self.catalog[idx].extensions.push(ext.clone());
1327 }
1328 self.catalog_by_extension.insert(ext.to_lowercase(), idx);
1330 }
1331 for filename in &lang_cfg.filenames {
1332 if is_glob_pattern(filename) {
1333 if !self.catalog[idx]
1334 .filename_globs
1335 .iter()
1336 .any(|f| f == filename)
1337 {
1338 self.catalog[idx].filename_globs.push(filename.clone());
1339 }
1340 } else {
1341 if !self.catalog[idx].filenames.iter().any(|f| f == filename) {
1342 self.catalog[idx].filenames.push(filename.clone());
1343 }
1344 self.catalog_by_filename.insert(filename.clone(), idx);
1345 }
1346 }
1347 }
1348 }
1349
1350 pub fn syntax_set(&self) -> &Arc<SyntaxSet> {
1352 &self.syntax_set
1353 }
1354
1355 pub fn syntax_set_arc(&self) -> Arc<SyntaxSet> {
1357 Arc::clone(&self.syntax_set)
1358 }
1359
1360 pub fn available_syntaxes(&self) -> Vec<&str> {
1362 self.syntax_set
1363 .syntaxes()
1364 .iter()
1365 .map(|s| s.name.as_str())
1366 .collect()
1367 }
1368
1369 pub fn available_grammar_info(&self) -> Vec<GrammarInfo> {
1376 let mut result: Vec<GrammarInfo> = self
1377 .catalog
1378 .iter()
1379 .map(|entry| GrammarInfo {
1380 name: entry.display_name.clone(),
1381 source: entry.source.clone(),
1382 file_extensions: entry.extensions.clone(),
1383 short_name: entry.short_name.clone(),
1384 })
1385 .collect();
1386 result.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
1387 result
1388 }
1389
1390 pub(crate) fn grammar_sources(&self) -> &HashMap<String, GrammarInfo> {
1392 &self.grammar_sources
1393 }
1394
1395 pub(crate) fn build_grammar_sources_from_syntax_set(
1399 syntax_set: &SyntaxSet,
1400 ) -> HashMap<String, GrammarInfo> {
1401 let mut sources = HashMap::new();
1402 for syntax in syntax_set.syntaxes() {
1403 sources.insert(
1404 syntax.name.clone(),
1405 GrammarInfo {
1406 name: syntax.name.clone(),
1407 source: GrammarSource::BuiltIn,
1408 file_extensions: syntax.file_extensions.clone(),
1409 short_name: None,
1410 },
1411 );
1412 }
1413 sources
1414 }
1415
1416 #[cfg(test)]
1418 pub(crate) fn user_extensions(&self) -> &HashMap<String, String> {
1419 &self.user_extensions
1420 }
1421
1422 #[cfg(test)]
1424 pub(crate) fn loaded_grammar_paths(&self) -> &[GrammarSpec] {
1425 &self.loaded_grammar_paths
1426 }
1427
1428 pub fn with_additional_grammars(
1442 base: &GrammarRegistry,
1443 additional: &[GrammarSpec],
1444 ) -> Option<Self> {
1445 tracing::info!(
1446 "[SYNTAX DEBUG] with_additional_grammars: adding {} grammars to base with {} syntaxes",
1447 additional.len(),
1448 base.syntax_set.syntaxes().len()
1449 );
1450
1451 let mut builder = (*base.syntax_set).clone().into_builder();
1455
1456 let mut user_extensions = base.user_extensions.clone();
1458
1459 let mut loaded_grammar_paths = base.loaded_grammar_paths.clone();
1461
1462 let mut grammar_sources = base.grammar_sources.clone();
1464
1465 for spec in additional {
1467 tracing::info!(
1468 "[SYNTAX DEBUG] loading new grammar file: lang='{}', path={:?}, extensions={:?}",
1469 spec.language,
1470 spec.path,
1471 spec.extensions
1472 );
1473 match Self::load_grammar_file(&spec.path) {
1474 Ok(syntax) => {
1475 let scope = syntax.scope.to_string();
1476 let syntax_name = syntax.name.clone();
1477 tracing::info!(
1478 "[SYNTAX DEBUG] grammar loaded successfully: name='{}', scope='{}'",
1479 syntax_name,
1480 scope
1481 );
1482 builder.add(syntax);
1483 tracing::info!(
1484 "Loaded grammar for '{}' from {:?} with extensions {:?}",
1485 spec.language,
1486 spec.path,
1487 spec.extensions
1488 );
1489 for ext in &spec.extensions {
1491 user_extensions.insert(ext.clone(), scope.clone());
1492 }
1493 grammar_sources.insert(
1495 syntax_name.clone(),
1496 GrammarInfo {
1497 name: syntax_name,
1498 source: GrammarSource::Plugin {
1499 plugin: spec.language.clone(),
1500 path: spec.path.clone(),
1501 },
1502 file_extensions: spec.extensions.clone(),
1503 short_name: None,
1504 },
1505 );
1506 loaded_grammar_paths.push(spec.clone());
1508 }
1509 Err(e) => {
1510 tracing::warn!(
1511 "Failed to load grammar for '{}' from {:?}: {}",
1512 spec.language,
1513 spec.path,
1514 e
1515 );
1516 }
1517 }
1518 }
1519
1520 let mut reg = Self {
1521 syntax_set: Arc::new(builder.build()),
1522 user_extensions,
1523 filename_scopes: base.filename_scopes.clone(),
1524 loaded_grammar_paths,
1525 grammar_sources,
1526 aliases: base.aliases.clone(),
1527 catalog: Vec::new(),
1528 catalog_by_name: HashMap::new(),
1529 catalog_by_extension: HashMap::new(),
1530 catalog_by_filename: HashMap::new(),
1531 applied_language_config: HashMap::new(),
1532 catalog_gen: 0,
1533 };
1534 reg.rebuild_catalog();
1535 Some(reg)
1536 }
1537
1538 pub(crate) fn load_grammar_file(path: &Path) -> Result<SyntaxDefinition, String> {
1544 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
1545
1546 match ext {
1547 "sublime-syntax" => {
1548 let content = std::fs::read_to_string(path)
1549 .map_err(|e| format!("Failed to read file: {}", e))?;
1550 SyntaxDefinition::load_from_str(
1551 &content,
1552 true,
1553 path.file_stem().and_then(|s| s.to_str()),
1554 )
1555 .map_err(|e| format!("Failed to parse sublime-syntax: {}", e))
1556 }
1557 _ => Err(format!(
1558 "Unsupported grammar format: .{}. Only .sublime-syntax is supported.",
1559 ext
1560 )),
1561 }
1562 }
1563}
1564
1565impl Default for GrammarRegistry {
1566 fn default() -> Self {
1567 let defaults = SyntaxSet::load_defaults_newlines();
1569 let mut builder = defaults.into_builder();
1570 Self::add_embedded_grammars(&mut builder);
1571 let syntax_set = builder.build();
1572 let filename_scopes = Self::build_filename_scopes();
1573 let extra_extensions = Self::build_extra_extensions();
1574
1575 let mut registry = Self::new(syntax_set, extra_extensions, filename_scopes);
1576 registry.populate_built_in_aliases();
1577 registry.rebuild_catalog();
1578 registry
1579 }
1580}
1581
1582#[derive(Debug, Deserialize)]
1585pub struct PackageManifest {
1586 #[serde(default)]
1587 pub contributes: Option<Contributes>,
1588}
1589
1590#[derive(Debug, Deserialize, Default)]
1591pub struct Contributes {
1592 #[serde(default)]
1593 pub languages: Vec<LanguageContribution>,
1594 #[serde(default)]
1595 pub grammars: Vec<GrammarContribution>,
1596}
1597
1598#[derive(Debug, Deserialize)]
1599pub struct LanguageContribution {
1600 pub id: String,
1601 #[serde(default)]
1602 pub extensions: Vec<String>,
1603}
1604
1605#[derive(Debug, Deserialize)]
1606pub struct GrammarContribution {
1607 pub language: String,
1608 #[serde(rename = "scopeName")]
1609 pub scope_name: String,
1610 pub path: String,
1611}
1612
1613#[cfg(test)]
1614mod tests {
1615 use super::*;
1616
1617 #[test]
1618 fn test_empty_registry() {
1619 let registry = GrammarRegistry::empty();
1620 assert!(!registry.available_syntaxes().is_empty());
1622 }
1623
1624 #[test]
1625 fn test_default_registry() {
1626 let registry = GrammarRegistry::default();
1627 assert!(!registry.available_syntaxes().is_empty());
1629 }
1630
1631 #[test]
1632 fn test_find_syntax_for_common_extensions() {
1633 let registry = GrammarRegistry::default();
1634
1635 let test_cases = [
1642 ("test.py", true),
1643 ("test.rs", true),
1644 ("test.js", false),
1645 ("test.json", true),
1646 ("test.md", true),
1647 ("test.html", true),
1648 ("test.css", true),
1649 ("test.gd", true),
1650 ("test.unknown_extension_xyz", false),
1651 ];
1652
1653 for (filename, should_exist) in test_cases {
1654 let path = Path::new(filename);
1655 let result = registry.find_syntax_for_file(path);
1656 assert_eq!(
1657 result.is_some(),
1658 should_exist,
1659 "Expected {:?} for {}",
1660 should_exist,
1661 filename
1662 );
1663 }
1664 }
1665
1666 #[test]
1667 fn test_racket_grammar_loaded() {
1668 let registry = GrammarRegistry::default();
1669 for filename in ["main.rkt", "data.rktd", "info.rktl", "doc.scrbl"] {
1670 let result = registry.find_syntax_for_file(Path::new(filename));
1671 assert!(
1672 result.is_some(),
1673 "Racket grammar should be available for {}",
1674 filename
1675 );
1676 let entry = registry.find_by_path(Path::new(filename), None).unwrap();
1677 assert_eq!(entry.display_name, "Racket", "for {}", filename);
1678 }
1679 }
1680
1681 #[test]
1682 fn test_syntax_set_arc() {
1683 let registry = GrammarRegistry::default();
1684 let arc1 = registry.syntax_set_arc();
1685 let arc2 = registry.syntax_set_arc();
1686 assert!(Arc::ptr_eq(&arc1, &arc2));
1688 }
1689
1690 #[test]
1691 fn test_shell_dotfiles_detection() {
1692 let registry = GrammarRegistry::default();
1693
1694 let shell_files = [".zshrc", ".zprofile", ".zshenv", ".bash_aliases"];
1696
1697 for filename in shell_files {
1698 let path = Path::new(filename);
1699 let result = registry.find_syntax_for_file(path);
1700 assert!(
1701 result.is_some(),
1702 "{} should be detected as a syntax",
1703 filename
1704 );
1705 let syntax = result.unwrap();
1706 assert!(
1708 syntax.name.to_lowercase().contains("bash")
1709 || syntax.name.to_lowercase().contains("shell"),
1710 "{} should be detected as shell/bash, got: {}",
1711 filename,
1712 syntax.name
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn test_pkgbuild_detection() {
1719 let registry = GrammarRegistry::default();
1720
1721 for filename in ["PKGBUILD", "APKBUILD"] {
1723 let path = Path::new(filename);
1724 let result = registry.find_syntax_for_file(path);
1725 assert!(
1726 result.is_some(),
1727 "{} should be detected as a syntax",
1728 filename
1729 );
1730 let syntax = result.unwrap();
1731 assert!(
1733 syntax.name.to_lowercase().contains("bash")
1734 || syntax.name.to_lowercase().contains("shell"),
1735 "{} should be detected as shell/bash, got: {}",
1736 filename,
1737 syntax.name
1738 );
1739 }
1740 }
1741
1742 #[test]
1743 fn test_find_syntax_with_glob_filenames() {
1744 let mut registry = GrammarRegistry::default();
1745 let mut languages = std::collections::HashMap::new();
1746 languages.insert(
1747 "shell-configs".to_string(),
1748 crate::config::LanguageConfig {
1749 extensions: vec!["sh".to_string()],
1750 filenames: vec!["*.conf".to_string(), "*rc".to_string()],
1751 grammar: "bash".to_string(),
1752 comment_prefix: Some("#".to_string()),
1753 auto_indent: true,
1754 auto_close: None,
1755 auto_surround: None,
1756 textmate_grammar: None,
1757 show_whitespace_tabs: true,
1758 line_wrap: None,
1759 wrap_column: None,
1760 page_view: None,
1761 page_width: None,
1762 use_tabs: None,
1763 tab_size: None,
1764 formatter: None,
1765 format_on_save: false,
1766 on_save: vec![],
1767 word_characters: None,
1768 indent: None,
1769 },
1770 );
1771 registry.apply_language_config(&languages);
1772
1773 assert!(
1774 registry
1775 .find_by_path(Path::new("nftables.conf"), None)
1776 .is_some(),
1777 "*.conf should match nftables.conf"
1778 );
1779 assert!(
1780 registry.find_by_path(Path::new("lfrc"), None).is_some(),
1781 "*rc should match lfrc"
1782 );
1783 let _ = registry.find_by_path(Path::new("randomfile"), None);
1785 }
1786
1787 #[test]
1788 fn test_find_syntax_with_path_glob_filenames() {
1789 let mut registry = GrammarRegistry::default();
1790 let mut languages = std::collections::HashMap::new();
1791 languages.insert(
1792 "shell-configs".to_string(),
1793 crate::config::LanguageConfig {
1794 extensions: vec!["sh".to_string()],
1795 filenames: vec!["/etc/**/rc.*".to_string()],
1796 grammar: "bash".to_string(),
1797 comment_prefix: Some("#".to_string()),
1798 auto_indent: true,
1799 auto_close: None,
1800 auto_surround: None,
1801 textmate_grammar: None,
1802 show_whitespace_tabs: true,
1803 line_wrap: None,
1804 wrap_column: None,
1805 page_view: None,
1806 page_width: None,
1807 use_tabs: None,
1808 tab_size: None,
1809 formatter: None,
1810 format_on_save: false,
1811 on_save: vec![],
1812 word_characters: None,
1813 indent: None,
1814 },
1815 );
1816 registry.apply_language_config(&languages);
1817
1818 assert!(
1819 registry
1820 .find_by_path(Path::new("/etc/rc.conf"), None)
1821 .is_some(),
1822 "/etc/**/rc.* should match /etc/rc.conf"
1823 );
1824 assert!(
1825 registry
1826 .find_by_path(Path::new("/etc/init/rc.local"), None)
1827 .is_some(),
1828 "/etc/**/rc.* should match /etc/init/rc.local"
1829 );
1830 let _ = registry.find_by_path(Path::new("/var/rc.conf"), None);
1831 }
1832
1833 #[test]
1834 fn test_exact_filename_takes_priority_over_glob() {
1835 let mut registry = GrammarRegistry::default();
1836 let mut languages = std::collections::HashMap::new();
1837
1838 languages.insert(
1840 "custom-lfrc".to_string(),
1841 crate::config::LanguageConfig {
1842 extensions: vec![],
1843 filenames: vec!["lfrc".to_string()],
1844 grammar: "python".to_string(),
1845 comment_prefix: Some("#".to_string()),
1846 auto_indent: true,
1847 auto_close: None,
1848 auto_surround: None,
1849 textmate_grammar: None,
1850 show_whitespace_tabs: true,
1851 line_wrap: None,
1852 wrap_column: None,
1853 page_view: None,
1854 page_width: None,
1855 use_tabs: None,
1856 tab_size: None,
1857 formatter: None,
1858 format_on_save: false,
1859 on_save: vec![],
1860 word_characters: None,
1861 indent: None,
1862 },
1863 );
1864
1865 languages.insert(
1867 "rc-files".to_string(),
1868 crate::config::LanguageConfig {
1869 extensions: vec![],
1870 filenames: vec!["*rc".to_string()],
1871 grammar: "bash".to_string(),
1872 comment_prefix: Some("#".to_string()),
1873 auto_indent: true,
1874 auto_close: None,
1875 auto_surround: None,
1876 textmate_grammar: None,
1877 show_whitespace_tabs: true,
1878 line_wrap: None,
1879 wrap_column: None,
1880 page_view: None,
1881 page_width: None,
1882 use_tabs: None,
1883 tab_size: None,
1884 formatter: None,
1885 format_on_save: false,
1886 on_save: vec![],
1887 word_characters: None,
1888 indent: None,
1889 },
1890 );
1891
1892 registry.apply_language_config(&languages);
1893
1894 let entry = registry.find_by_path(Path::new("lfrc"), None).unwrap();
1896 assert!(
1897 entry.display_name.to_lowercase().contains("python"),
1898 "exact match should win over glob, got: {}",
1899 entry.display_name
1900 );
1901 }
1902
1903 #[test]
1904 fn test_built_in_aliases_resolve() {
1905 let registry = GrammarRegistry::default();
1906
1907 let syntax = registry.find_syntax_by_name("bash");
1909 assert!(syntax.is_some(), "alias 'bash' should resolve");
1910 assert_eq!(syntax.unwrap().name, "Bourne Again Shell (bash)");
1911
1912 let syntax = registry.find_syntax_by_name("cpp");
1914 assert!(syntax.is_some(), "alias 'cpp' should resolve");
1915 assert_eq!(syntax.unwrap().name, "C++");
1916
1917 let syntax = registry.find_syntax_by_name("csharp");
1919 assert!(syntax.is_some(), "alias 'csharp' should resolve");
1920 assert_eq!(syntax.unwrap().name, "C#");
1921
1922 let syntax = registry.find_syntax_by_name("sh");
1924 assert!(syntax.is_some(), "alias 'sh' should resolve");
1925 assert_eq!(syntax.unwrap().name, "Bourne Again Shell (bash)");
1926
1927 let syntax = registry.find_syntax_by_name("proto");
1929 assert!(syntax.is_some(), "alias 'proto' should resolve");
1930 assert_eq!(syntax.unwrap().name, "Protocol Buffers");
1931 }
1932
1933 #[test]
1934 fn test_alias_case_insensitive_input() {
1935 let registry = GrammarRegistry::default();
1936
1937 let syntax = registry.find_syntax_by_name("BASH");
1939 assert!(
1940 syntax.is_some(),
1941 "alias 'BASH' should resolve case-insensitively"
1942 );
1943 assert_eq!(syntax.unwrap().name, "Bourne Again Shell (bash)");
1944
1945 let syntax = registry.find_syntax_by_name("Cpp");
1946 assert!(
1947 syntax.is_some(),
1948 "alias 'Cpp' should resolve case-insensitively"
1949 );
1950 assert_eq!(syntax.unwrap().name, "C++");
1951 }
1952
1953 #[test]
1954 fn test_full_name_still_works() {
1955 let registry = GrammarRegistry::default();
1956
1957 let syntax = registry.find_syntax_by_name("Bourne Again Shell (bash)");
1959 assert!(syntax.is_some(), "full name should still resolve");
1960 assert_eq!(syntax.unwrap().name, "Bourne Again Shell (bash)");
1961
1962 let syntax = registry.find_syntax_by_name("bourne again shell (bash)");
1964 assert!(
1965 syntax.is_some(),
1966 "case-insensitive full name should resolve"
1967 );
1968 assert_eq!(syntax.unwrap().name, "Bourne Again Shell (bash)");
1969 }
1970
1971 #[test]
1972 fn test_alias_does_not_shadow_full_names() {
1973 let registry = GrammarRegistry::default();
1974
1975 let syntax = registry.find_syntax_by_name("rust");
1977 assert!(syntax.is_some());
1978 assert_eq!(syntax.unwrap().name, "Rust");
1979
1980 let syntax = registry.find_syntax_by_name("go");
1982 assert!(syntax.is_some());
1983 assert_eq!(syntax.unwrap().name, "Go");
1984 }
1985
1986 #[test]
1987 fn test_register_alias_rejects_collision() {
1988 let mut registry = GrammarRegistry::default();
1989
1990 assert!(registry.register_alias("myalias", "Rust"));
1992 assert!(!registry.register_alias("myalias", "Go"));
1993
1994 assert!(registry.register_alias("myalias", "Rust"));
1996 }
1997
1998 #[test]
1999 fn test_register_alias_rejects_nonexistent_target() {
2000 let mut registry = GrammarRegistry::default();
2001 assert!(!registry.register_alias("nope", "Nonexistent Grammar"));
2002 }
2003
2004 #[test]
2005 fn test_register_alias_skips_existing_grammar_name() {
2006 let mut registry = GrammarRegistry::default();
2007
2008 assert!(!registry.register_alias("rust", "Rust"));
2010 assert!(registry.find_syntax_by_name("rust").is_some());
2012 }
2013
2014 #[test]
2015 fn test_available_grammar_info_includes_short_names() {
2016 let registry = GrammarRegistry::default();
2017 let infos = registry.available_grammar_info();
2018
2019 let bash_info = infos.iter().find(|g| g.name == "Bourne Again Shell (bash)");
2020 assert!(bash_info.is_some(), "bash grammar should be in the list");
2021 let bash_info = bash_info.unwrap();
2022 assert!(
2023 bash_info.short_name.is_some(),
2024 "bash grammar should have a short_name"
2025 );
2026 assert_eq!(bash_info.short_name.as_deref(), Some("sh"));
2028 }
2029
2030 #[test]
2031 fn test_catalog_contains_each_language_once() {
2032 let registry = GrammarRegistry::default();
2033 let catalog = registry.catalog();
2034
2035 let mut seen = std::collections::HashSet::new();
2037 for entry in catalog {
2038 let key = entry.display_name.to_lowercase();
2039 assert!(
2040 seen.insert(key.clone()),
2041 "duplicate catalog entry for display_name={:?}",
2042 entry.display_name
2043 );
2044 }
2045
2046 let ts = registry
2049 .find_by_name("TypeScript")
2050 .expect("TypeScript must be in the catalog");
2051 assert!(ts.engines.syntect.is_none());
2052 assert_eq!(
2053 ts.engines.tree_sitter,
2054 Some(fresh_languages::Language::TypeScript)
2055 );
2056 assert_eq!(ts.language_id, "typescript");
2057 assert!(ts.extensions.iter().any(|e| e == "ts"));
2058
2059 for name in ["Rust", "Python"] {
2062 let entry = registry
2063 .find_by_name(name)
2064 .unwrap_or_else(|| panic!("{} must be in the catalog", name));
2065 assert!(
2066 entry.engines.syntect.is_some(),
2067 "{} should have a syntect index",
2068 name
2069 );
2070 assert!(
2071 entry.engines.tree_sitter.is_some(),
2072 "{} should also have a tree-sitter language",
2073 name
2074 );
2075 let by_id = registry
2078 .find_by_name(&entry.language_id)
2079 .expect("language_id should resolve");
2080 assert_eq!(by_id.display_name, entry.display_name);
2081 }
2082
2083 let js = registry
2089 .find_by_name("JavaScript")
2090 .expect("JavaScript must be in the catalog");
2091 assert!(
2092 js.engines.syntect.is_none(),
2093 "JavaScript must not be routed to the syntect engine (issue #899)"
2094 );
2095 assert_eq!(
2096 js.engines.tree_sitter,
2097 Some(fresh_languages::Language::JavaScript),
2098 "JavaScript must carry the tree-sitter language"
2099 );
2100
2101 let gdscript = registry
2102 .find_by_path(Path::new("player.gd"), None)
2103 .expect("player.gd should resolve to GDScript");
2104 assert_eq!(gdscript.display_name, "GDScript");
2105 assert_eq!(gdscript.language_id, "gdscript");
2106 assert!(
2107 gdscript.engines.syntect.is_some(),
2108 "GDScript should use the embedded Syntect grammar"
2109 );
2110 assert!(
2111 gdscript.engines.tree_sitter.is_none(),
2112 "GDScript must not carry a tree-sitter parser"
2113 );
2114 }
2115
2116 #[test]
2117 fn test_catalog_find_by_path_and_extension() {
2118 let registry = GrammarRegistry::default();
2119 let ts = registry
2120 .find_by_path(Path::new("foo.ts"), None)
2121 .expect("foo.ts should resolve");
2122 assert_eq!(ts.display_name, "TypeScript");
2123 let rs = registry.find_by_extension("rs").expect("rs should resolve");
2124 assert_eq!(rs.display_name, "Rust");
2125 }
2126
2127 #[test]
2128 fn test_smali_embedded_grammar_loads_and_resolves() {
2129 let syntax = SyntaxDefinition::load_from_str(SMALI_GRAMMAR, true, Some("Smali"))
2130 .expect("Smali grammar should parse");
2131 assert!(syntax.file_extensions.iter().any(|ext| ext == "smali"));
2132
2133 let registry = GrammarRegistry::default();
2134 let entry = registry
2135 .find_by_path(Path::new("MainActivity.smali"), None)
2136 .expect("Smali files should resolve");
2137 assert_eq!(entry.display_name, "Smali");
2138 assert!(entry.engines.syntect.is_some());
2139 assert!(entry.engines.tree_sitter.is_none());
2140 }
2141
2142 fn lang_cfg(
2144 grammar: &str,
2145 extensions: &[&str],
2146 filenames: &[&str],
2147 ) -> crate::config::LanguageConfig {
2148 crate::config::LanguageConfig {
2149 extensions: extensions.iter().map(|s| s.to_string()).collect(),
2150 filenames: filenames.iter().map(|s| s.to_string()).collect(),
2151 grammar: grammar.to_string(),
2152 comment_prefix: None,
2153 auto_indent: true,
2154 auto_close: None,
2155 auto_surround: None,
2156 textmate_grammar: None,
2157 show_whitespace_tabs: true,
2158 line_wrap: None,
2159 wrap_column: None,
2160 page_view: None,
2161 page_width: None,
2162 use_tabs: None,
2163 tab_size: None,
2164 formatter: None,
2165 format_on_save: false,
2166 on_save: vec![],
2167 word_characters: None,
2168 indent: None,
2169 }
2170 }
2171
2172 #[test]
2176 fn test_user_alias_resolves_via_find_by_name() {
2177 let mut registry = GrammarRegistry::default();
2178 let mut languages = std::collections::HashMap::new();
2179 languages.insert("mylang".to_string(), lang_cfg("Rust", &[], &[]));
2180 registry.apply_language_config(&languages);
2181
2182 let entry = registry
2183 .find_by_name("mylang")
2184 .expect("user-declared alias 'mylang' must resolve");
2185 assert_eq!(entry.display_name, "Rust");
2186 }
2187
2188 #[test]
2192 fn test_register_alias_preserves_applied_language_config() {
2193 let mut registry = GrammarRegistry::default();
2194 let mut languages = std::collections::HashMap::new();
2195 languages.insert(
2196 "shell-configs".to_string(),
2197 lang_cfg("bash", &["myconf"], &["*.myconf"]),
2198 );
2199 registry.apply_language_config(&languages);
2200
2201 assert!(registry.find_by_extension("myconf").is_some());
2203 assert!(
2204 registry
2205 .find_by_path(Path::new("foo.myconf"), None)
2206 .is_some(),
2207 "glob should match before register_alias"
2208 );
2209
2210 registry.register_alias("mycustom", "Rust");
2212
2213 assert!(
2214 registry.find_by_extension("myconf").is_some(),
2215 "config extension must survive register_alias"
2216 );
2217 assert!(
2218 registry
2219 .find_by_path(Path::new("foo.myconf"), None)
2220 .is_some(),
2221 "glob must survive register_alias"
2222 );
2223 }
2224
2225 #[test]
2229 fn test_from_syntax_name_preserves_canonical_display_name() {
2230 use crate::primitives::detected_language::DetectedLanguage;
2231 let registry = GrammarRegistry::default();
2232 let languages = std::collections::HashMap::new();
2233
2234 let detected = DetectedLanguage::from_syntax_name("BASH", ®istry, &languages)
2235 .expect("BASH should resolve via alias");
2236 assert_eq!(
2237 detected.display_name, "Bourne Again Shell (bash)",
2238 "display_name must be canonical, not user-typed"
2239 );
2240 }
2241
2242 #[test]
2246 fn test_config_only_language_appears_in_catalog() {
2247 let mut registry = GrammarRegistry::default();
2248 let mut languages = std::collections::HashMap::new();
2249 languages.insert("elvish".to_string(), lang_cfg("elvish", &["elv"], &[]));
2250 registry.apply_language_config(&languages);
2251
2252 let entry = registry
2253 .find_by_name("elvish")
2254 .expect("elvish should be in the catalog after apply_language_config");
2255 assert!(entry.engines.syntect.is_none());
2256 assert!(entry.engines.tree_sitter.is_none());
2257 assert_eq!(entry.language_id, "elvish");
2258 assert!(entry.extensions.iter().any(|e| e == "elv"));
2259 }
2260
2261 #[test]
2262 fn test_fish_extension_resolves_to_fish_grammar_not_bash() {
2263 let registry = GrammarRegistry::default();
2266 let entry = registry
2267 .find_by_extension("fish")
2268 .expect(".fish should resolve to a grammar entry");
2269
2270 assert_eq!(entry.language_id, "fish");
2271 assert_eq!(entry.display_name, "Fish");
2272 assert!(entry.engines.syntect.is_some());
2273 }
2274
2275 #[test]
2280 fn test_config_extension_overrides_builtin() {
2281 let mut registry = GrammarRegistry::default();
2282 assert_eq!(
2284 registry.find_by_extension("js").unwrap().display_name,
2285 "JavaScript"
2286 );
2287
2288 let mut languages = std::collections::HashMap::new();
2289 languages.insert(
2290 "ts-overlay".to_string(),
2291 lang_cfg("TypeScript", &["js"], &[]),
2292 );
2293 registry.apply_language_config(&languages);
2294
2295 assert_eq!(
2296 registry.find_by_extension("js").unwrap().display_name,
2297 "TypeScript",
2298 "user-config extension must win over built-in"
2299 );
2300 }
2301
2302 #[test]
2309 fn test_bare_filename_resolves_via_find_by_path() {
2310 let registry = GrammarRegistry::default();
2311 for (filename, expected_substr) in [
2312 ("Gemfile", "ruby"),
2313 ("Rakefile", "ruby"),
2314 ("Vagrantfile", "ruby"),
2315 ("Makefile", "makefile"),
2316 ("GNUmakefile", "makefile"),
2317 ] {
2318 let entry = registry
2319 .find_by_path(Path::new(filename), None)
2320 .unwrap_or_else(|| panic!("{} must resolve via catalog", filename));
2321 assert!(
2322 entry.display_name.to_lowercase().contains(expected_substr),
2323 "{} should resolve to {} grammar, got {}",
2324 filename,
2325 expected_substr,
2326 entry.display_name
2327 );
2328 }
2329 }
2330
2331 #[test]
2336 fn test_jsx_resolves_to_javascript() {
2337 let registry = GrammarRegistry::default();
2338 let entry = registry
2339 .find_by_path(Path::new("foo.jsx"), None)
2340 .expect("foo.jsx must resolve");
2341 assert_eq!(entry.display_name, "JavaScript");
2342 }
2343
2344 #[test]
2349 fn test_rebuild_catalog_replays_language_config() {
2350 let mut registry = GrammarRegistry::default();
2351 let mut languages = std::collections::HashMap::new();
2352 languages.insert(
2353 "myshell".to_string(),
2354 lang_cfg("bash", &["myext"], &["*.myglob"]),
2355 );
2356 registry.apply_language_config(&languages);
2357 assert!(registry.find_by_extension("myext").is_some());
2358 assert!(registry
2359 .find_by_path(Path::new("foo.myglob"), None)
2360 .is_some());
2361
2362 registry.rebuild_catalog();
2365 assert!(
2366 registry.find_by_extension("myext").is_some(),
2367 "rebuild_catalog must replay applied user config"
2368 );
2369 assert!(
2370 registry
2371 .find_by_path(Path::new("foo.myglob"), None)
2372 .is_some(),
2373 "rebuild_catalog must replay user globs"
2374 );
2375 }
2376
2377 #[test]
2380 fn test_apply_language_config_idempotent() {
2381 let mut registry = GrammarRegistry::default();
2382 let mut languages = std::collections::HashMap::new();
2383 languages.insert(
2384 "shell-cfg".to_string(),
2385 lang_cfg("bash", &["myconf"], &["*.myconf"]),
2386 );
2387
2388 registry.apply_language_config(&languages);
2389 let first_extensions = registry
2390 .find_by_name("bash")
2391 .unwrap()
2392 .extensions
2393 .iter()
2394 .filter(|e| e == &"myconf")
2395 .count();
2396 let first_globs = registry
2397 .find_by_name("bash")
2398 .unwrap()
2399 .filename_globs
2400 .iter()
2401 .filter(|g| g == &"*.myconf")
2402 .count();
2403 assert_eq!(first_extensions, 1);
2404 assert_eq!(first_globs, 1);
2405
2406 registry.apply_language_config(&languages);
2408 let second_extensions = registry
2409 .find_by_name("bash")
2410 .unwrap()
2411 .extensions
2412 .iter()
2413 .filter(|e| e == &"myconf")
2414 .count();
2415 let second_globs = registry
2416 .find_by_name("bash")
2417 .unwrap()
2418 .filename_globs
2419 .iter()
2420 .filter(|g| g == &"*.myconf")
2421 .count();
2422 assert_eq!(second_extensions, 1, "extensions must not duplicate");
2423 assert_eq!(second_globs, 1, "globs must not duplicate");
2424 }
2425
2426 #[test]
2432 fn test_julia_adjoint_does_not_start_string() {
2433 use syntect::parsing::{ParseState, ScopeStack};
2434
2435 let registry = GrammarRegistry::default();
2436 let syntax_set = registry.syntax_set();
2437 let syntax = registry
2438 .find_syntax_by_name("Julia")
2439 .expect("Julia grammar must be loaded");
2440 let mut state = ParseState::new(syntax);
2441 let mut scopes = ScopeStack::new();
2442
2443 let lines = ["x = A'\n", "function foo()\n", "end\n"];
2445 let mut keyword_line_in_string = false;
2446 let mut found_function_keyword = false;
2447
2448 for line in &lines {
2449 let ops = state.parse_line(line, syntax_set).unwrap();
2450 let mut op_iter = ops.iter().peekable();
2452 for (byte_idx, _) in line.char_indices() {
2453 while let Some((offset, op)) = op_iter.peek() {
2454 if *offset <= byte_idx {
2455 scopes.apply(op).unwrap();
2456 op_iter.next();
2457 } else {
2458 break;
2459 }
2460 }
2461 let in_string = scopes
2462 .as_slice()
2463 .iter()
2464 .any(|s| s.build_string().starts_with("string."));
2465 let is_function_kw = line[byte_idx..].starts_with("function");
2466 if is_function_kw && in_string {
2467 keyword_line_in_string = true;
2468 }
2469 if is_function_kw && !in_string {
2470 found_function_keyword = true;
2471 }
2472 }
2473 for (_, op) in op_iter {
2475 scopes.apply(op).unwrap();
2476 }
2477 }
2478
2479 assert!(
2480 !keyword_line_in_string,
2481 "the `function` keyword after an adjoint operator must not be inside a string scope"
2482 );
2483 assert!(
2484 found_function_keyword,
2485 "test harness must have reached the `function` keyword"
2486 );
2487 }
2488
2489 #[test]
2492 fn test_julia_char_literal_is_recognized() {
2493 use syntect::parsing::{ParseState, ScopeStack};
2494
2495 let registry = GrammarRegistry::default();
2496 let syntax_set = registry.syntax_set();
2497 let syntax = registry
2498 .find_syntax_by_name("Julia")
2499 .expect("Julia grammar must be loaded");
2500 let mut state = ParseState::new(syntax);
2501 let mut scopes = ScopeStack::new();
2502
2503 let line = "x = 'a'\n";
2504 let ops = state.parse_line(line, syntax_set).unwrap();
2505 let mut saw_constant_or_string_at_quote = false;
2506 let mut op_iter = ops.iter().peekable();
2507 for (byte_idx, _) in line.char_indices() {
2508 while let Some((offset, op)) = op_iter.peek() {
2509 if *offset <= byte_idx {
2510 scopes.apply(op).unwrap();
2511 op_iter.next();
2512 } else {
2513 break;
2514 }
2515 }
2516 if byte_idx == 5 {
2517 let scoped = scopes.as_slice().iter().any(|s| {
2519 let str = s.build_string();
2520 str.starts_with("constant.") || str.starts_with("string.")
2521 });
2522 if scoped {
2523 saw_constant_or_string_at_quote = true;
2524 }
2525 }
2526 }
2527 assert!(
2528 saw_constant_or_string_at_quote,
2529 "char literal 'a' must receive a constant/string scope"
2530 );
2531 }
2532
2533 #[test]
2537 fn test_tree_sitter_bridge() {
2538 assert_eq!(
2539 tree_sitter_for_syntect_name("Bourne Again Shell (bash)"),
2540 Some(fresh_languages::Language::Bash)
2541 );
2542 assert_eq!(
2543 tree_sitter_for_syntect_name("Rust"),
2544 Some(fresh_languages::Language::Rust)
2545 );
2546 assert_eq!(tree_sitter_for_syntect_name("GDScript"), None);
2547 assert_eq!(tree_sitter_for_syntect_name("Nushell"), None);
2549 assert_eq!(tree_sitter_for_syntect_name("does-not-exist"), None);
2551 }
2552}