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}
142
143fn rust_normalize(name: &str) -> String {
144 name.replace('-', "_")
145}
146
147static RUST_MANIFEST: ManifestSpec = ManifestSpec {
148 filename: "Cargo.toml",
149 name_key: "name",
150 self_names: &["crate"],
151 normalize: rust_normalize,
152};
153
154fn split_all(path: &str, separators: &[&str]) -> Vec<String> {
155 let mut segments = vec![path.to_string()];
156 for sep in separators {
157 segments = segments
158 .iter()
159 .flat_map(|s| s.split(sep).map(str::to_string))
160 .collect();
161 }
162 segments.into_iter().filter(|s| !s.is_empty()).collect()
163}
164
165fn dirname_segments(file: &str) -> Vec<String> {
166 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
167 segments.pop();
168 segments
169}
170
171fn rust_absolutize(path: &str, file: &str) -> Vec<String> {
174 let mut module = rust_module_path(file);
175 let mut rest = path;
176 if let Some(r) = rest.strip_prefix("self::") {
177 rest = r;
178 } else {
179 while let Some(r) = rest.strip_prefix("super::") {
180 module.pop();
181 rest = r;
182 }
183 if rest.len() == path.len() {
184 if path.starts_with("crate::") {
185 return split_all(path, &["::", "."]);
186 }
187 module.extend(split_all(path, &["::", "."]));
190 return module;
191 }
192 }
193 module.extend(split_all(rest, &["::", "."]));
194 module
195}
196
197fn go_absolutize(path: &str, _file: &str) -> Vec<String> {
198 split_all(path, &["/", "."])
199}
200
201fn proto_absolutize(path: &str, _file: &str) -> Vec<String> {
206 split_all(path.strip_suffix(".proto").unwrap_or(path), &["/", "."])
207}
208
209fn python_absolutize(path: &str, file: &str) -> Vec<String> {
212 let dots = path.len() - path.trim_start_matches('.').len();
213 if dots == 0 {
214 return split_all(path, &["."]);
215 }
216 let mut base = dirname_segments(file);
217 for _ in 1..dots {
218 base.pop();
219 }
220 base.extend(split_all(&path[dots..], &["."]));
221 base
222}
223
224fn typescript_absolutize(path: &str, file: &str) -> Vec<String> {
226 if !path.starts_with('.') {
227 return split_all(path, &["/", "."]);
228 }
229 let mut base = dirname_segments(file);
230 let mut rest = path;
231 if let Some(r) = rest.strip_prefix('/') {
234 base.clear();
235 rest = r;
236 }
237 loop {
238 if let Some(r) = rest.strip_prefix("./") {
239 rest = r;
240 } else if let Some(r) = rest.strip_prefix("../") {
241 base.pop();
242 rest = r;
243 } else {
244 break;
245 }
246 }
247 base.extend(split_all(rest, &["/"]));
248 base
249}
250
251fn rust_grammar() -> Language {
252 tree_sitter_rust::LANGUAGE.into()
253}
254
255fn go_grammar() -> Language {
256 tree_sitter_go::LANGUAGE.into()
257}
258
259fn rust_module_path(file: &str) -> Vec<String> {
263 let trimmed = file.strip_suffix(".rs").unwrap_or(file);
264 let after_src = trimmed.rsplit_once("src/").map_or(trimmed, |(_, r)| r);
265 let mut segments = vec!["crate".to_string()];
266 for seg in after_src.split('/') {
267 if !matches!(seg, "lib" | "main" | "mod" | "") {
268 segments.push(seg.to_string());
269 }
270 }
271 segments
272}
273
274fn go_module_path(file: &str) -> Vec<String> {
276 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
277 segments.pop(); segments
279}
280
281fn python_grammar() -> Language {
282 tree_sitter_python::LANGUAGE.into()
283}
284
285fn bash_grammar() -> Language {
286 tree_sitter_bash::LANGUAGE.into()
287}
288
289fn bash_module_path(file: &str) -> Vec<String> {
292 let trimmed = file
293 .strip_suffix(".sh")
294 .or_else(|| file.strip_suffix(".bash"))
295 .unwrap_or(file);
296 trimmed
297 .split('/')
298 .filter(|s| !s.is_empty())
299 .map(str::to_string)
300 .collect()
301}
302
303fn bash_absolutize(path: &str, file: &str) -> Vec<String> {
306 let trimmed = path.trim();
307 let dir_relative = [
308 "$(dirname \"$0\")/",
309 "$(dirname $0)/",
310 "${BASH_SOURCE%/*}/",
311 "./",
312 ]
313 .iter()
314 .find_map(|p| trimmed.strip_prefix(p));
315 let stripped = |s: &str| {
316 s.strip_suffix(".sh")
317 .or_else(|| s.strip_suffix(".bash"))
318 .unwrap_or(s)
319 .to_string()
320 };
321 match dir_relative {
322 Some(rest) => {
323 let mut base = dirname_segments(file);
324 base.extend(rest.split('/').filter(|s| !s.is_empty()).map(stripped));
325 base
326 }
327 None => trimmed
328 .split('/')
329 .filter(|s| !s.is_empty() && *s != ".")
330 .map(stripped)
331 .collect(),
332 }
333}
334
335fn cpp_grammar() -> Language {
336 tree_sitter_cpp::LANGUAGE.into()
337}
338
339fn cpp_module_path(file: &str) -> Vec<String> {
342 let trimmed = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
343 trimmed
344 .split('/')
345 .filter(|s| !s.is_empty())
346 .map(str::to_string)
347 .collect()
348}
349
350fn cpp_absolutize(path: &str, file: &str) -> Vec<String> {
354 let trimmed = path.trim().trim_matches(['<', '>']);
355 let no_ext = trimmed.rsplit_once('.').map_or(trimmed, |(stem, ext)| {
356 if matches!(
357 ext,
358 "h" | "hh" | "hpp" | "hxx" | "cpp" | "cc" | "cxx" | "inl"
359 ) {
360 stem
361 } else {
362 trimmed
363 }
364 });
365 if let Some(rest) = no_ext.strip_prefix("./") {
366 let mut base = dirname_segments(file);
367 base.extend(
368 rest.split('/')
369 .filter(|s| !s.is_empty())
370 .map(str::to_string),
371 );
372 return base;
373 }
374 no_ext
377 .replace("->", ".")
378 .split(['/', ':', '.'])
379 .filter(|s| !s.is_empty())
380 .map(str::to_string)
381 .collect()
382}
383
384fn proto_grammar() -> Language {
385 tree_sitter_proto::LANGUAGE.into()
386}
387
388fn proto_module_path(file: &str) -> Vec<String> {
392 let trimmed = file.strip_suffix(".proto").unwrap_or(file);
393 trimmed
394 .split('/')
395 .filter(|s| !s.is_empty())
396 .map(str::to_string)
397 .collect()
398}
399
400fn typescript_grammar() -> Language {
401 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
402}
403
404fn python_module_path(file: &str) -> Vec<String> {
406 let trimmed = file.strip_suffix(".py").unwrap_or(file);
407 trimmed
408 .split('/')
409 .filter(|s| !matches!(*s, "__init__" | ""))
410 .map(str::to_string)
411 .collect()
412}
413
414fn typescript_module_path(file: &str) -> Vec<String> {
416 let trimmed = file
417 .strip_suffix(".tsx")
418 .or_else(|| file.strip_suffix(".ts"))
419 .unwrap_or(file);
420 trimmed
421 .split('/')
422 .filter(|s| !matches!(*s, "index" | ""))
423 .map(str::to_string)
424 .collect()
425}
426
427fn javascript_grammar() -> Language {
428 tree_sitter_javascript::LANGUAGE.into()
429}
430
431fn c_grammar() -> Language {
432 tree_sitter_c::LANGUAGE.into()
433}
434
435fn java_grammar() -> Language {
436 tree_sitter_java::LANGUAGE.into()
437}
438
439fn csharp_grammar() -> Language {
440 tree_sitter_c_sharp::LANGUAGE.into()
441}
442
443fn csharp_module_path(file: &str) -> Vec<String> {
450 let mut segments: Vec<String> = file.split('/').map(str::to_string).collect();
451 segments.pop(); segments
453}
454
455fn markdown_grammar() -> Language {
456 tree_sitter_md::LANGUAGE.into()
457}
458
459fn markdown_inline_grammar() -> Language {
460 tree_sitter_md::INLINE_LANGUAGE.into()
461}
462
463static MARKDOWN_INLINE: InlineSpec = InlineSpec {
467 grammar: markdown_inline_grammar,
468 query_source: include_str!("../queries/markdown-inline.scm"),
469 container_kinds: &["inline"],
470};
471
472fn markdown_absolutize(path: &str, file: &str) -> Vec<String> {
476 let mut base = dirname_segments(file);
477 let mut rest = path;
478 if let Some(r) = rest.strip_prefix('/') {
481 base.clear();
482 rest = r;
483 }
484 loop {
485 if let Some(r) = rest.strip_prefix("./") {
486 rest = r;
487 } else if let Some(r) = rest.strip_prefix("../") {
488 base.pop();
489 rest = r;
490 } else {
491 break;
492 }
493 }
494 let rest = rest
495 .strip_suffix(".md")
496 .or_else(|| rest.strip_suffix(".markdown"))
497 .unwrap_or(rest);
498 base.extend(
499 rest.split('/')
500 .filter(|s| !s.is_empty())
501 .map(str::to_string),
502 );
503 base
504}
505
506fn markdown_module_path(file: &str) -> Vec<String> {
508 let trimmed = file
509 .strip_suffix(".md")
510 .or_else(|| file.strip_suffix(".markdown"))
511 .unwrap_or(file);
512 trimmed
513 .split('/')
514 .filter(|s| !s.is_empty())
515 .map(str::to_string)
516 .collect()
517}
518
519fn sql_grammar() -> Language {
520 tree_sitter_sequel::LANGUAGE.into()
521}
522
523fn javascript_module_path(file: &str) -> Vec<String> {
524 let trimmed = file
525 .strip_suffix(".jsx")
526 .or_else(|| file.strip_suffix(".mjs"))
527 .or_else(|| file.strip_suffix(".cjs"))
528 .or_else(|| file.strip_suffix(".js"))
529 .unwrap_or(file);
530 trimmed
531 .split('/')
532 .filter(|s| !matches!(*s, "index" | ""))
533 .map(str::to_string)
534 .collect()
535}
536
537fn javascript_absolutize(path: &str, file: &str) -> Vec<String> {
538 typescript_absolutize(path, file)
539}
540
541fn c_module_path(file: &str) -> Vec<String> {
542 cpp_module_path(file)
543}
544
545fn c_absolutize(path: &str, file: &str) -> Vec<String> {
546 cpp_absolutize(path, file)
547}
548
549fn java_module_path(file: &str) -> Vec<String> {
556 dirname_segments(file)
557 .into_iter()
558 .filter(|s| !s.is_empty())
559 .collect()
560}
561
562fn java_absolutize(path: &str, _file: &str) -> Vec<String> {
566 let head = path.split('(').next().unwrap_or(path);
567 head.split('.')
568 .map(str::trim)
569 .filter(|s| !s.is_empty())
570 .map(str::to_string)
571 .collect()
572}
573
574fn sql_module_path(file: &str) -> Vec<String> {
578 let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
579 dir.split('/')
580 .filter(|s| !s.is_empty())
581 .map(str::to_string)
582 .collect()
583}
584
585fn dotted_absolutize(path: &str, _file: &str) -> Vec<String> {
588 path.trim()
589 .split('.')
590 .filter(|s| !s.is_empty())
591 .map(str::to_string)
592 .collect()
593}
594
595pub static LANGUAGES: &[LanguageSpec] = &[
596 LanguageSpec {
597 name: "rust",
598 extensions: &["rs"],
599 grammar: rust_grammar,
600 query_source: include_str!("../queries/rust.scm"),
601 comment_kinds: &["line_comment", "block_comment"],
602 module_path: rust_module_path,
603 path_separators: &["::", "."],
604 absolutize: rust_absolutize,
605 receivers: &["self", "Self"],
606 doc_skip_kinds: &[],
607 manifest: Some(&RUST_MANIFEST),
608 inline: None,
609 file_refs: false,
610 },
611 LanguageSpec {
612 name: "go",
613 extensions: &["go"],
614 grammar: go_grammar,
615 query_source: include_str!("../queries/go.scm"),
616 comment_kinds: &["comment"],
617 module_path: go_module_path,
618 path_separators: &["/", "."],
619 absolutize: go_absolutize,
620 receivers: &[],
621 doc_skip_kinds: &[],
622 manifest: None,
623 inline: None,
624 file_refs: false,
625 },
626 LanguageSpec {
627 name: "python",
628 extensions: &["py"],
629 grammar: python_grammar,
630 query_source: include_str!("../queries/python.scm"),
631 comment_kinds: &["comment"],
632 module_path: python_module_path,
633 path_separators: &["."],
634 absolutize: python_absolutize,
635 receivers: &["self", "cls"],
636 doc_skip_kinds: &[],
637 manifest: None,
638 inline: None,
639 file_refs: false,
640 },
641 LanguageSpec {
642 name: "typescript",
643 extensions: &["ts", "tsx"],
644 grammar: typescript_grammar,
645 query_source: include_str!("../queries/typescript.scm"),
646 comment_kinds: &["comment"],
647 module_path: typescript_module_path,
648 path_separators: &["/", "."],
649 absolutize: typescript_absolutize,
650 receivers: &["this"],
651 doc_skip_kinds: &[],
652 manifest: None,
653 inline: None,
654 file_refs: false,
655 },
656 LanguageSpec {
657 name: "bash",
658 extensions: &["sh", "bash"],
659 grammar: bash_grammar,
660 query_source: include_str!("../queries/bash.scm"),
661 comment_kinds: &["comment"],
662 module_path: bash_module_path,
663 path_separators: &["/"],
664 absolutize: bash_absolutize,
665 receivers: &[],
666 doc_skip_kinds: &[],
667 manifest: None,
668 inline: None,
669 file_refs: false,
670 },
671 LanguageSpec {
672 name: "proto",
673 extensions: &["proto"],
674 grammar: proto_grammar,
675 query_source: include_str!("../queries/proto.scm"),
676 comment_kinds: &["comment"],
677 module_path: proto_module_path,
678 path_separators: &["/", "."],
679 absolutize: proto_absolutize,
680 receivers: &[],
681 doc_skip_kinds: &[],
682 manifest: None,
683 inline: None,
684 file_refs: false,
685 },
686 LanguageSpec {
687 name: "cpp",
688 extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
689 grammar: cpp_grammar,
690 query_source: include_str!("../queries/cpp.scm"),
691 comment_kinds: &["comment"],
692 module_path: cpp_module_path,
693 path_separators: &["/", "::"],
694 absolutize: cpp_absolutize,
695 receivers: &["this"],
696 doc_skip_kinds: &["expression_statement"],
697 manifest: None,
698 inline: None,
699 file_refs: false,
700 },
701 LanguageSpec {
702 name: "javascript",
703 extensions: &["js", "jsx", "mjs", "cjs"],
704 grammar: javascript_grammar,
705 query_source: include_str!("../queries/javascript.scm"),
706 comment_kinds: &["comment"],
707 module_path: javascript_module_path,
708 path_separators: &["/", "."],
709 absolutize: javascript_absolutize,
710 receivers: &["this"],
711 doc_skip_kinds: &[],
712 manifest: None,
713 inline: None,
714 file_refs: false,
715 },
716 LanguageSpec {
717 name: "c",
718 extensions: &["c"],
719 grammar: c_grammar,
720 query_source: include_str!("../queries/c.scm"),
721 comment_kinds: &["comment"],
722 module_path: c_module_path,
723 path_separators: &["/"],
724 absolutize: c_absolutize,
725 receivers: &[],
726 doc_skip_kinds: &[],
727 manifest: None,
728 inline: None,
729 file_refs: false,
730 },
731 LanguageSpec {
732 name: "java",
733 extensions: &["java"],
734 grammar: java_grammar,
735 query_source: include_str!("../queries/java.scm"),
736 comment_kinds: &["line_comment", "block_comment"],
737 module_path: java_module_path,
738 path_separators: &["."],
739 absolutize: java_absolutize,
740 receivers: &["this"],
741 doc_skip_kinds: &[],
742 manifest: None,
743 inline: None,
744 file_refs: false,
745 },
746 LanguageSpec {
747 name: "csharp",
748 extensions: &["cs"],
749 grammar: csharp_grammar,
750 query_source: include_str!("../queries/csharp.scm"),
751 comment_kinds: &["comment"],
752 module_path: csharp_module_path,
753 path_separators: &["."],
754 absolutize: dotted_absolutize,
755 receivers: &["this", "base"],
756 doc_skip_kinds: &[],
757 manifest: None,
758 inline: None,
759 file_refs: false,
760 },
761 LanguageSpec {
762 name: "sql",
763 extensions: &["sql"],
764 grammar: sql_grammar,
765 query_source: include_str!("../queries/sql.scm"),
766 comment_kinds: &["comment", "marginalia"],
767 module_path: sql_module_path,
768 path_separators: &["."],
769 absolutize: dotted_absolutize,
770 receivers: &[],
771 doc_skip_kinds: &[],
772 manifest: None,
773 inline: None,
774 file_refs: false,
775 },
776 LanguageSpec {
777 name: "markdown",
778 extensions: &["md", "markdown"],
779 grammar: markdown_grammar,
780 query_source: include_str!("../queries/markdown.scm"),
781 comment_kinds: &[],
782 module_path: markdown_module_path,
783 path_separators: &["/"],
784 absolutize: markdown_absolutize,
785 receivers: &[],
786 doc_skip_kinds: &[],
787 manifest: None,
788 inline: Some(&MARKDOWN_INLINE),
789 file_refs: true,
790 },
791];
792
793pub fn spec_for_path(path: &str) -> Option<&'static LanguageSpec> {
795 let ext = path.rsplit('.').next()?;
796 LANGUAGES.iter().find(|spec| spec.extensions.contains(&ext))
797}