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