1use tree_sitter::Language;
2
3pub struct ManifestSpec {
43 pub filename: &'static str,
45 pub name_key: &'static str,
48 pub self_names: &'static [&'static str],
50 pub normalize: fn(&str) -> String,
53}
54
55#[derive(Debug, Clone)]
58pub struct ModuleRoot {
59 pub name: String,
60 pub dir: String,
62 pub language: &'static str,
63}
64
65pub fn manifest_root(rel_path: &str, content: &str) -> Option<ModuleRoot> {
68 let base = rel_path.rsplit('/').next()?;
69 let spec = LANGUAGES
70 .iter()
71 .find(|l| l.manifest.is_some_and(|m| m.filename == base))?;
72 let m = spec.manifest?;
73 let name = content.lines().find_map(|line| {
74 let rest = line.trim().strip_prefix(m.name_key)?;
75 let rest = rest.trim_start();
76 let rest = rest.strip_prefix('=').unwrap_or(rest).trim();
77 let name = rest.trim_matches('"').trim();
78 (!name.is_empty() && !name.contains(' ')).then(|| name.to_string())
79 })?;
80 let dir = rel_path
81 .rsplit_once('/')
82 .map(|(d, _)| d.to_string())
83 .unwrap_or_default();
84 Some(ModuleRoot {
85 name: (m.normalize)(&name),
86 dir,
87 language: spec.name,
88 })
89}
90
91pub struct InlineSpec {
97 pub grammar: fn() -> Language,
98 pub query_source: &'static str,
99 pub container_kinds: &'static [&'static str],
102}
103
104pub struct LanguageSpec {
105 pub name: &'static str,
106 pub extensions: &'static [&'static str],
107 pub grammar: fn() -> Language,
108 pub query_source: &'static str,
109 pub comment_kinds: &'static [&'static str],
110 pub module_path: fn(&str) -> Vec<String>,
114 pub path_separators: &'static [&'static str],
116 pub absolutize: fn(path: &str, file: &str) -> Vec<String>,
120 pub receivers: &'static [&'static str],
123 pub doc_skip_kinds: &'static [&'static str],
127 pub manifest: Option<&'static ManifestSpec>,
130 pub inline: Option<&'static InlineSpec>,
134 pub file_refs: bool,
141 pub implicit_interfaces: bool,
146}
147
148fn rust_normalize(name: &str) -> String {
149 name.replace('-', "_")
150}
151
152static RUST_MANIFEST: ManifestSpec = ManifestSpec {
153 filename: "Cargo.toml",
154 name_key: "name",
155 self_names: &["crate"],
156 normalize: rust_normalize,
157};
158
159fn identity_normalize(name: &str) -> String {
160 name.to_string()
161}
162
163static GO_MANIFEST: ManifestSpec = ManifestSpec {
167 filename: "go.mod",
168 name_key: "module",
169 self_names: &[],
170 normalize: identity_normalize,
171};
172
173fn split_all(path: &str, separators: &[&str]) -> Vec<String> {
174 let mut segments = vec![path.to_string()];
175 for sep in separators {
176 segments = segments
177 .iter()
178 .flat_map(|s| s.split(sep).map(str::to_string))
179 .collect();
180 }
181 segments.into_iter().filter(|s| !s.is_empty()).collect()
182}
183
184fn dirname_segments(file: &str) -> Vec<String> {
185 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
186 segments.pop();
187 segments
188}
189
190fn rust_absolutize(path: &str, file: &str) -> Vec<String> {
193 let mut module = rust_module_path(file);
194 let mut rest = path;
195 if let Some(r) = rest.strip_prefix("self::") {
196 rest = r;
197 } else {
198 while let Some(r) = rest.strip_prefix("super::") {
199 module.pop();
200 rest = r;
201 }
202 if rest.len() == path.len() {
203 if path.starts_with("crate::") {
204 return split_all(path, &["::", "."]);
205 }
206 module.extend(split_all(path, &["::", "."]));
209 return module;
210 }
211 }
212 module.extend(split_all(rest, &["::", "."]));
213 module
214}
215
216fn go_absolutize(path: &str, _file: &str) -> Vec<String> {
217 split_all(path, &["/", "."])
218}
219
220fn proto_absolutize(path: &str, _file: &str) -> Vec<String> {
225 split_all(path.strip_suffix(".proto").unwrap_or(path), &["/", "."])
226}
227
228fn python_absolutize(path: &str, file: &str) -> Vec<String> {
231 let dots = path.len() - path.trim_start_matches('.').len();
232 if dots == 0 {
233 return split_all(path, &["."]);
234 }
235 let mut base = dirname_segments(file);
236 for _ in 1..dots {
237 base.pop();
238 }
239 base.extend(split_all(&path[dots..], &["."]));
240 base
241}
242
243fn typescript_absolutize(path: &str, file: &str) -> Vec<String> {
245 if !path.starts_with('.') {
246 return split_all(path, &["/", "."]);
247 }
248 let mut base = dirname_segments(file);
249 let mut rest = path;
250 if let Some(r) = rest.strip_prefix('/') {
253 base.clear();
254 rest = r;
255 }
256 loop {
257 if let Some(r) = rest.strip_prefix("./") {
258 rest = r;
259 } else if let Some(r) = rest.strip_prefix("../") {
260 base.pop();
261 rest = r;
262 } else {
263 break;
264 }
265 }
266 base.extend(split_all(rest, &["/"]));
267 base
268}
269
270fn rust_grammar() -> Language {
271 tree_sitter_rust::LANGUAGE.into()
272}
273
274fn go_grammar() -> Language {
275 tree_sitter_go::LANGUAGE.into()
276}
277
278fn rust_module_path(file: &str) -> Vec<String> {
282 let trimmed = file.strip_suffix(".rs").unwrap_or(file);
283 let after_src = trimmed.rsplit_once("src/").map_or(trimmed, |(_, r)| r);
284 let mut segments = vec!["crate".to_string()];
285 for seg in after_src.split('/') {
286 if !matches!(seg, "lib" | "main" | "mod" | "") {
287 segments.push(seg.to_string());
288 }
289 }
290 segments
291}
292
293fn go_module_path(file: &str) -> Vec<String> {
295 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
296 segments.pop(); segments
298}
299
300fn python_grammar() -> Language {
301 tree_sitter_python::LANGUAGE.into()
302}
303
304fn bash_grammar() -> Language {
305 tree_sitter_bash::LANGUAGE.into()
306}
307
308fn bash_module_path(file: &str) -> Vec<String> {
311 let trimmed = file
312 .strip_suffix(".sh")
313 .or_else(|| file.strip_suffix(".bash"))
314 .unwrap_or(file);
315 trimmed
316 .split('/')
317 .filter(|s| !s.is_empty())
318 .map(str::to_string)
319 .collect()
320}
321
322fn bash_absolutize(path: &str, file: &str) -> Vec<String> {
325 let trimmed = path.trim();
326 let dir_relative = [
327 "$(dirname \"$0\")/",
328 "$(dirname $0)/",
329 "${BASH_SOURCE%/*}/",
330 "./",
331 ]
332 .iter()
333 .find_map(|p| trimmed.strip_prefix(p));
334 let stripped = |s: &str| {
335 s.strip_suffix(".sh")
336 .or_else(|| s.strip_suffix(".bash"))
337 .unwrap_or(s)
338 .to_string()
339 };
340 match dir_relative {
341 Some(rest) => {
342 let mut base = dirname_segments(file);
343 base.extend(rest.split('/').filter(|s| !s.is_empty()).map(stripped));
344 base
345 }
346 None => trimmed
347 .split('/')
348 .filter(|s| !s.is_empty() && *s != ".")
349 .map(stripped)
350 .collect(),
351 }
352}
353
354fn cpp_grammar() -> Language {
355 tree_sitter_cpp::LANGUAGE.into()
356}
357
358fn cpp_module_path(file: &str) -> Vec<String> {
361 let trimmed = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
362 trimmed
363 .split('/')
364 .filter(|s| !s.is_empty())
365 .map(str::to_string)
366 .collect()
367}
368
369fn cpp_absolutize(path: &str, file: &str) -> Vec<String> {
373 let trimmed = path.trim().trim_matches(['<', '>']);
374 let no_ext = trimmed.rsplit_once('.').map_or(trimmed, |(stem, ext)| {
375 if matches!(
376 ext,
377 "h" | "hh" | "hpp" | "hxx" | "cpp" | "cc" | "cxx" | "inl"
378 ) {
379 stem
380 } else {
381 trimmed
382 }
383 });
384 if let Some(rest) = no_ext.strip_prefix("./") {
385 let mut base = dirname_segments(file);
386 base.extend(
387 rest.split('/')
388 .filter(|s| !s.is_empty())
389 .map(str::to_string),
390 );
391 return base;
392 }
393 no_ext
396 .replace("->", ".")
397 .split(['/', ':', '.'])
398 .filter(|s| !s.is_empty())
399 .map(str::to_string)
400 .collect()
401}
402
403fn proto_grammar() -> Language {
404 tree_sitter_proto::LANGUAGE.into()
405}
406
407fn proto_module_path(file: &str) -> Vec<String> {
411 let trimmed = file.strip_suffix(".proto").unwrap_or(file);
412 trimmed
413 .split('/')
414 .filter(|s| !s.is_empty())
415 .map(str::to_string)
416 .collect()
417}
418
419fn typescript_grammar() -> Language {
420 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
421}
422
423fn python_module_path(file: &str) -> Vec<String> {
425 let trimmed = file.strip_suffix(".py").unwrap_or(file);
426 trimmed
427 .split('/')
428 .filter(|s| !matches!(*s, "__init__" | ""))
429 .map(str::to_string)
430 .collect()
431}
432
433fn typescript_module_path(file: &str) -> Vec<String> {
435 let trimmed = file
436 .strip_suffix(".tsx")
437 .or_else(|| file.strip_suffix(".ts"))
438 .unwrap_or(file);
439 trimmed
440 .split('/')
441 .filter(|s| !matches!(*s, "index" | ""))
442 .map(str::to_string)
443 .collect()
444}
445
446fn javascript_grammar() -> Language {
447 tree_sitter_javascript::LANGUAGE.into()
448}
449
450fn c_grammar() -> Language {
451 tree_sitter_c::LANGUAGE.into()
452}
453
454fn java_grammar() -> Language {
455 tree_sitter_java::LANGUAGE.into()
456}
457
458fn csharp_grammar() -> Language {
459 tree_sitter_c_sharp::LANGUAGE.into()
460}
461
462fn csharp_module_path(file: &str) -> Vec<String> {
469 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
470 segments.pop(); segments
472}
473
474fn markdown_grammar() -> Language {
475 tree_sitter_md::LANGUAGE.into()
476}
477
478fn markdown_inline_grammar() -> Language {
479 tree_sitter_md::INLINE_LANGUAGE.into()
480}
481
482static MARKDOWN_INLINE: InlineSpec = InlineSpec {
486 grammar: markdown_inline_grammar,
487 query_source: include_str!("../queries/markdown-inline.scm"),
488 container_kinds: &["inline"],
489};
490
491fn markdown_absolutize(path: &str, file: &str) -> Vec<String> {
495 let mut base = dirname_segments(file);
496 let mut rest = path;
497 if let Some(r) = rest.strip_prefix('/') {
500 base.clear();
501 rest = r;
502 }
503 loop {
504 if let Some(r) = rest.strip_prefix("./") {
505 rest = r;
506 } else if let Some(r) = rest.strip_prefix("../") {
507 base.pop();
508 rest = r;
509 } else {
510 break;
511 }
512 }
513 let rest = rest
514 .strip_suffix(".md")
515 .or_else(|| rest.strip_suffix(".markdown"))
516 .unwrap_or(rest);
517 base.extend(
518 rest.split('/')
519 .filter(|s| !s.is_empty())
520 .map(str::to_string),
521 );
522 base
523}
524
525fn markdown_module_path(file: &str) -> Vec<String> {
527 let trimmed = file
528 .strip_suffix(".md")
529 .or_else(|| file.strip_suffix(".markdown"))
530 .unwrap_or(file);
531 trimmed
532 .split('/')
533 .filter(|s| !s.is_empty())
534 .map(str::to_string)
535 .collect()
536}
537
538fn sql_grammar() -> Language {
539 tree_sitter_sequel::LANGUAGE.into()
540}
541
542fn javascript_module_path(file: &str) -> Vec<String> {
543 let trimmed = file
544 .strip_suffix(".jsx")
545 .or_else(|| file.strip_suffix(".mjs"))
546 .or_else(|| file.strip_suffix(".cjs"))
547 .or_else(|| file.strip_suffix(".js"))
548 .unwrap_or(file);
549 trimmed
550 .split('/')
551 .filter(|s| !matches!(*s, "index" | ""))
552 .map(str::to_string)
553 .collect()
554}
555
556fn javascript_absolutize(path: &str, file: &str) -> Vec<String> {
557 typescript_absolutize(path, file)
558}
559
560fn c_module_path(file: &str) -> Vec<String> {
561 cpp_module_path(file)
562}
563
564fn c_absolutize(path: &str, file: &str) -> Vec<String> {
565 cpp_absolutize(path, file)
566}
567
568fn java_module_path(file: &str) -> Vec<String> {
575 dirname_segments(file)
576 .into_iter()
577 .filter(|s| !s.is_empty())
578 .collect()
579}
580
581fn java_absolutize(path: &str, _file: &str) -> Vec<String> {
585 let head = path.split('(').next().unwrap_or(path);
586 head.split('.')
587 .map(str::trim)
588 .filter(|s| !s.is_empty())
589 .map(str::to_string)
590 .collect()
591}
592
593fn sql_module_path(file: &str) -> Vec<String> {
597 let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
598 dir.split('/')
599 .filter(|s| !s.is_empty())
600 .map(str::to_string)
601 .collect()
602}
603
604fn dotted_absolutize(path: &str, _file: &str) -> Vec<String> {
607 path.trim()
608 .split('.')
609 .filter(|s| !s.is_empty())
610 .map(str::to_string)
611 .collect()
612}
613
614pub static LANGUAGES: &[LanguageSpec] = &[
615 LanguageSpec {
616 name: "rust",
617 extensions: &["rs"],
618 grammar: rust_grammar,
619 query_source: include_str!("../queries/rust.scm"),
620 comment_kinds: &["line_comment", "block_comment"],
621 module_path: rust_module_path,
622 path_separators: &["::", "."],
623 absolutize: rust_absolutize,
624 receivers: &["self", "Self"],
625 doc_skip_kinds: &[],
626 manifest: Some(&RUST_MANIFEST),
627 inline: None,
628 file_refs: false,
629 implicit_interfaces: false,
630 },
631 LanguageSpec {
632 name: "go",
633 extensions: &["go"],
634 grammar: go_grammar,
635 query_source: include_str!("../queries/go.scm"),
636 comment_kinds: &["comment"],
637 module_path: go_module_path,
638 path_separators: &["/", "."],
639 absolutize: go_absolutize,
640 receivers: &[],
641 doc_skip_kinds: &[],
642 manifest: Some(&GO_MANIFEST),
643 inline: None,
644 file_refs: false,
645 implicit_interfaces: true,
646 },
647 LanguageSpec {
648 name: "python",
649 extensions: &["py"],
650 grammar: python_grammar,
651 query_source: include_str!("../queries/python.scm"),
652 comment_kinds: &["comment"],
653 module_path: python_module_path,
654 path_separators: &["."],
655 absolutize: python_absolutize,
656 receivers: &["self", "cls"],
657 doc_skip_kinds: &[],
658 manifest: None,
659 inline: None,
660 file_refs: false,
661 implicit_interfaces: false,
662 },
663 LanguageSpec {
664 name: "typescript",
665 extensions: &["ts", "tsx"],
666 grammar: typescript_grammar,
667 query_source: include_str!("../queries/typescript.scm"),
668 comment_kinds: &["comment"],
669 module_path: typescript_module_path,
670 path_separators: &["/", "."],
671 absolutize: typescript_absolutize,
672 receivers: &["this"],
673 doc_skip_kinds: &[],
674 manifest: None,
675 inline: None,
676 file_refs: false,
677 implicit_interfaces: false,
678 },
679 LanguageSpec {
680 name: "bash",
681 extensions: &["sh", "bash"],
682 grammar: bash_grammar,
683 query_source: include_str!("../queries/bash.scm"),
684 comment_kinds: &["comment"],
685 module_path: bash_module_path,
686 path_separators: &["/"],
687 absolutize: bash_absolutize,
688 receivers: &[],
689 doc_skip_kinds: &[],
690 manifest: None,
691 inline: None,
692 file_refs: false,
693 implicit_interfaces: false,
694 },
695 LanguageSpec {
696 name: "proto",
697 extensions: &["proto"],
698 grammar: proto_grammar,
699 query_source: include_str!("../queries/proto.scm"),
700 comment_kinds: &["comment"],
701 module_path: proto_module_path,
702 path_separators: &["/", "."],
703 absolutize: proto_absolutize,
704 receivers: &[],
705 doc_skip_kinds: &[],
706 manifest: None,
707 inline: None,
708 file_refs: false,
709 implicit_interfaces: false,
710 },
711 LanguageSpec {
712 name: "cpp",
713 extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
714 grammar: cpp_grammar,
715 query_source: include_str!("../queries/cpp.scm"),
716 comment_kinds: &["comment"],
717 module_path: cpp_module_path,
718 path_separators: &["/", "::"],
719 absolutize: cpp_absolutize,
720 receivers: &["this"],
721 doc_skip_kinds: &["expression_statement"],
722 manifest: None,
723 inline: None,
724 file_refs: false,
725 implicit_interfaces: false,
726 },
727 LanguageSpec {
728 name: "javascript",
729 extensions: &["js", "jsx", "mjs", "cjs"],
730 grammar: javascript_grammar,
731 query_source: include_str!("../queries/javascript.scm"),
732 comment_kinds: &["comment"],
733 module_path: javascript_module_path,
734 path_separators: &["/", "."],
735 absolutize: javascript_absolutize,
736 receivers: &["this"],
737 doc_skip_kinds: &[],
738 manifest: None,
739 inline: None,
740 file_refs: false,
741 implicit_interfaces: false,
742 },
743 LanguageSpec {
744 name: "c",
745 extensions: &["c"],
746 grammar: c_grammar,
747 query_source: include_str!("../queries/c.scm"),
748 comment_kinds: &["comment"],
749 module_path: c_module_path,
750 path_separators: &["/"],
751 absolutize: c_absolutize,
752 receivers: &[],
753 doc_skip_kinds: &[],
754 manifest: None,
755 inline: None,
756 file_refs: false,
757 implicit_interfaces: false,
758 },
759 LanguageSpec {
760 name: "java",
761 extensions: &["java"],
762 grammar: java_grammar,
763 query_source: include_str!("../queries/java.scm"),
764 comment_kinds: &["line_comment", "block_comment"],
765 module_path: java_module_path,
766 path_separators: &["."],
767 absolutize: java_absolutize,
768 receivers: &["this"],
769 doc_skip_kinds: &[],
770 manifest: None,
771 inline: None,
772 file_refs: false,
773 implicit_interfaces: false,
774 },
775 LanguageSpec {
776 name: "csharp",
777 extensions: &["cs"],
778 grammar: csharp_grammar,
779 query_source: include_str!("../queries/csharp.scm"),
780 comment_kinds: &["comment"],
781 module_path: csharp_module_path,
782 path_separators: &["."],
783 absolutize: dotted_absolutize,
784 receivers: &["this", "base"],
785 doc_skip_kinds: &[],
786 manifest: None,
787 inline: None,
788 file_refs: false,
789 implicit_interfaces: false,
790 },
791 LanguageSpec {
792 name: "sql",
793 extensions: &["sql"],
794 grammar: sql_grammar,
795 query_source: include_str!("../queries/sql.scm"),
796 comment_kinds: &["comment", "marginalia"],
797 module_path: sql_module_path,
798 path_separators: &["."],
799 absolutize: dotted_absolutize,
800 receivers: &[],
801 doc_skip_kinds: &[],
802 manifest: None,
803 inline: None,
804 file_refs: false,
805 implicit_interfaces: false,
806 },
807 LanguageSpec {
808 name: "markdown",
809 extensions: &["md", "markdown"],
810 grammar: markdown_grammar,
811 query_source: include_str!("../queries/markdown.scm"),
812 comment_kinds: &[],
813 module_path: markdown_module_path,
814 path_separators: &["/"],
815 absolutize: markdown_absolutize,
816 receivers: &[],
817 doc_skip_kinds: &[],
818 manifest: None,
819 inline: Some(&MARKDOWN_INLINE),
820 file_refs: true,
821 implicit_interfaces: false,
822 },
823];
824
825pub fn spec_for_path(path: &str) -> Option<&'static LanguageSpec> {
827 let ext = path.rsplit('.').next()?;
828 LANGUAGES.iter().find(|spec| spec.extensions.contains(&ext))
829}