1use crate::config::CodeGraphConfig;
2use crate::types::*;
3use regex::Regex;
4use std::path::Path;
5use tree_sitter::{Node as SyntaxNode, Parser};
6
7pub fn should_include_file(path: &Path, config: &CodeGraphConfig) -> bool {
8 let s = path.to_string_lossy().replace('\\', "/");
9 if s.starts_with(".codegraph/") {
10 return false;
11 }
12 if config.exclude.iter().any(|p| glob_match(p, &s)) {
13 return false;
14 }
15 config.include.iter().any(|p| glob_match(p, &s))
16}
17
18fn glob_match(pattern: &str, path: &str) -> bool {
19 let suffix = pattern.strip_prefix("**/*.");
20 if let Some(ext) = suffix {
21 return path.ends_with(&format!(".{}", ext));
22 }
23 if let Some(dir) = pattern
24 .strip_prefix("**/")
25 .and_then(|p| p.strip_suffix("/**"))
26 {
27 return path.contains(&format!("{}/", dir)) || path == dir;
28 }
29 if let Some(suffix) = pattern.strip_prefix("**/") {
30 return path.ends_with(suffix);
31 }
32 pattern == path
33}
34
35pub fn detect_language(path: &Path, _source: &str) -> Language {
36 let name = path
37 .file_name()
38 .and_then(|s| s.to_str())
39 .unwrap_or_default()
40 .to_lowercase();
41 if name == "moon.mod.json" || name == "moon.pkg.json" || name == "moon.pkg" {
42 return Language::MoonBit;
43 }
44 if name.ends_with(".mbt.md") {
45 return Language::MoonBit;
46 }
47 match path
48 .extension()
49 .and_then(|s| s.to_str())
50 .unwrap_or_default()
51 .to_lowercase()
52 .as_str()
53 {
54 "ts" => Language::TypeScript,
55 "tsx" => Language::Tsx,
56 "js" | "mjs" | "cjs" => Language::JavaScript,
57 "jsx" => Language::Jsx,
58 "py" | "pyw" => Language::Python,
59 "go" => Language::Go,
60 "rs" => Language::Rust,
61 "java" => Language::Java,
62 "c" | "h" => Language::C,
63 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Language::Cpp,
64 "cs" => Language::CSharp,
65 "php" => Language::Php,
66 "rb" | "rake" => Language::Ruby,
67 "swift" => Language::Swift,
68 "kt" | "kts" => Language::Kotlin,
69 "dart" => Language::Dart,
70 "svelte" => Language::Svelte,
71 "vue" => Language::Vue,
72 "liquid" => Language::Liquid,
73 "pas" | "dpr" | "dpk" | "lpr" | "dfm" | "fmx" => Language::Pascal,
74 "scala" | "sc" => Language::Scala,
75 "mbt" | "mbti" => Language::MoonBit,
76 _ => Language::Unknown,
77 }
78}
79
80pub fn extract_from_source(path: &Path, source: &str, language: Language) -> ExtractionResult {
81 let file_path = path.to_string_lossy().replace('\\', "/");
82 let now = now_ms();
83 let mut nodes = vec![Node {
84 id: format!("file:{}", file_path),
85 kind: NodeKind::File,
86 name: path
87 .file_name()
88 .and_then(|s| s.to_str())
89 .unwrap_or(&file_path)
90 .to_string(),
91 qualified_name: file_path.clone(),
92 file_path: file_path.clone(),
93 language,
94 start_line: 1,
95 end_line: source.lines().count().max(1) as i64,
96 start_column: 0,
97 end_column: 0,
98 docstring: None,
99 signature: None,
100 visibility: None,
101 is_exported: false,
102 is_async: false,
103 is_static: false,
104 is_abstract: false,
105 updated_at: now,
106 }];
107 let mut edges = Vec::new();
108 let mut refs = Vec::new();
109
110 match language {
111 Language::Rust => extract_rust(&file_path, source, now, &mut nodes, &mut edges, &mut refs),
112 Language::MoonBit => {
113 extract_moonbit(&file_path, source, now, &mut nodes, &mut edges, &mut refs)
114 }
115 _ => extract_generic(
116 &file_path, source, language, now, &mut nodes, &mut edges, &mut refs,
117 ),
118 }
119
120 ExtractionResult {
121 nodes,
122 edges,
123 unresolved_references: refs,
124 }
125}
126
127fn extract_rust(
128 file_path: &str,
129 source: &str,
130 now: i64,
131 nodes: &mut Vec<Node>,
132 edges: &mut Vec<Edge>,
133 refs: &mut Vec<UnresolvedReference>,
134) {
135 if try_extract_rust_tree_sitter(file_path, source, now, nodes, edges, refs) {
136 return;
137 }
138
139 add_regex_nodes(
140 file_path,
141 source,
142 Language::Rust,
143 now,
144 nodes,
145 edges,
146 r"(?m)^\s*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*([^{;]*)",
147 NodeKind::Function,
148 );
149 add_regex_nodes(
150 file_path,
151 source,
152 Language::Rust,
153 now,
154 nodes,
155 edges,
156 r"(?m)^\s*(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)",
157 NodeKind::Struct,
158 );
159 add_regex_nodes(
160 file_path,
161 source,
162 Language::Rust,
163 now,
164 nodes,
165 edges,
166 r"(?m)^\s*(pub(?:\([^)]*\))?\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)",
167 NodeKind::Trait,
168 );
169 add_regex_nodes(
170 file_path,
171 source,
172 Language::Rust,
173 now,
174 nodes,
175 edges,
176 r"(?m)^\s*(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_][A-Za-z0-9_]*)",
177 NodeKind::Enum,
178 );
179 add_regex_nodes(
180 file_path,
181 source,
182 Language::Rust,
183 now,
184 nodes,
185 edges,
186 r"(?m)^\s*(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_][A-Za-z0-9_]*)",
187 NodeKind::TypeAlias,
188 );
189
190 let use_re = Regex::new(r"(?m)^\s*use\s+([^;]+);").unwrap();
191 for cap in use_re.captures_iter(source) {
192 let full = cap.get(1).unwrap();
193 let root = full
194 .as_str()
195 .split("::")
196 .next()
197 .unwrap_or(full.as_str())
198 .trim_matches('{')
199 .trim();
200 let node = make_node(
201 file_path,
202 Language::Rust,
203 NodeKind::Import,
204 root,
205 line_for(source, full.start()),
206 0,
207 now,
208 Some(format!("use {};", full.as_str())),
209 );
210 add_contains(nodes, edges, &node);
211 refs.push(unresolved(
212 &nodes[0].id,
213 root,
214 EdgeKind::Imports,
215 file_path,
216 Language::Rust,
217 node.start_line,
218 ));
219 nodes.push(node);
220 }
221
222 let impl_re = Regex::new(
223 r"(?m)^\s*impl(?:<[^>]+>)?\s+([A-Za-z_][A-Za-z0-9_:]*)\s+for\s+([A-Za-z_][A-Za-z0-9_]*)",
224 )
225 .unwrap();
226 for cap in impl_re.captures_iter(source) {
227 let trait_name = cap.get(1).unwrap().as_str().rsplit("::").next().unwrap();
228 let type_name = cap.get(2).unwrap().as_str();
229 if let Some(src) = nodes
230 .iter()
231 .find(|n| n.name == type_name && matches!(n.kind, NodeKind::Struct | NodeKind::Enum))
232 .map(|n| n.id.clone())
233 {
234 refs.push(unresolved(
235 &src,
236 trait_name,
237 EdgeKind::Implements,
238 file_path,
239 Language::Rust,
240 line_for(source, cap.get(1).unwrap().start()),
241 ));
242 }
243 }
244 add_call_refs(
245 file_path,
246 source,
247 Language::Rust,
248 nodes,
249 refs,
250 r"([A-Za-z_][A-Za-z0-9_:]*)\s*\(",
251 );
252}
253
254fn extract_moonbit(
255 file_path: &str,
256 source: &str,
257 now: i64,
258 nodes: &mut Vec<Node>,
259 edges: &mut Vec<Edge>,
260 refs: &mut Vec<UnresolvedReference>,
261) {
262 if file_path.ends_with("moon.mod.json")
263 || file_path.ends_with("moon.pkg.json")
264 || file_path.ends_with("moon.pkg")
265 {
266 extract_moonbit_metadata(file_path, source, now, nodes, edges, refs);
267 return;
268 }
269
270 let source = if file_path.ends_with(".mbt.md") {
271 extract_mbt_markdown_code_with_padding(source)
272 } else {
273 source.to_string()
274 };
275
276 if try_extract_moonbit_tree_sitter(file_path, &source, now, nodes, edges, refs) {
277 extract_moonbit_sol_routes(file_path, &source, now, nodes, edges, refs);
278 return;
279 }
280
281 add_regex_nodes(
282 file_path,
283 &source,
284 Language::MoonBit,
285 now,
286 nodes,
287 edges,
288 r"(?m)^\s*(pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*([^{]*)",
289 NodeKind::Function,
290 );
291 add_regex_nodes(
292 file_path,
293 &source,
294 Language::MoonBit,
295 now,
296 nodes,
297 edges,
298 r"(?m)^\s*(pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)\s*([^{]*)",
299 NodeKind::Method,
300 );
301 add_regex_nodes(
302 file_path,
303 &source,
304 Language::MoonBit,
305 now,
306 nodes,
307 edges,
308 r"(?m)^\s*(pub\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)",
309 NodeKind::Struct,
310 );
311 add_regex_nodes(
312 file_path,
313 &source,
314 Language::MoonBit,
315 now,
316 nodes,
317 edges,
318 r"(?m)^\s*(pub\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)",
319 NodeKind::Trait,
320 );
321 add_regex_nodes(
322 file_path,
323 &source,
324 Language::MoonBit,
325 now,
326 nodes,
327 edges,
328 r"(?m)^\s*(pub\s+)?enum\s+([A-Za-z_][A-Za-z0-9_]*)",
329 NodeKind::Enum,
330 );
331 add_regex_nodes(
332 file_path,
333 &source,
334 Language::MoonBit,
335 now,
336 nodes,
337 edges,
338 r"(?m)^\s*(pub\s+)?type\s+([A-Za-z_][A-Za-z0-9_]*)",
339 NodeKind::TypeAlias,
340 );
341 add_regex_nodes(
342 file_path,
343 &source,
344 Language::MoonBit,
345 now,
346 nodes,
347 edges,
348 r"(?m)^\s*(pub\s+)?let\s+([A-Za-z_][A-Za-z0-9_]*)",
349 NodeKind::Variable,
350 );
351
352 let import_re =
353 Regex::new(r#"(?m)^\s*import\s+([@\w/.\-]+)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?"#)
354 .unwrap();
355 for cap in import_re.captures_iter(&source) {
356 let package = cap.get(1).unwrap().as_str();
357 let name = cap.get(2).map(|m| m.as_str()).unwrap_or(package);
358 let node = make_node(
359 file_path,
360 Language::MoonBit,
361 NodeKind::Import,
362 name,
363 line_for(&source, cap.get(0).unwrap().start()),
364 0,
365 now,
366 Some(cap.get(0).unwrap().as_str().to_string()),
367 );
368 add_contains(nodes, edges, &node);
369 refs.push(unresolved(
370 &nodes[0].id,
371 name,
372 EdgeKind::Imports,
373 file_path,
374 Language::MoonBit,
375 node.start_line,
376 ));
377 nodes.push(node);
378 }
379 add_call_refs(
380 file_path,
381 &source,
382 Language::MoonBit,
383 nodes,
384 refs,
385 r"([@A-Za-z_][@A-Za-z0-9_:/]*)\s*\(",
386 );
387 extract_moonbit_sol_routes(file_path, &source, now, nodes, edges, refs);
388}
389
390fn extract_moonbit_sol_routes(
391 file_path: &str,
392 source: &str,
393 now: i64,
394 nodes: &mut Vec<Node>,
395 edges: &mut Vec<Edge>,
396 refs: &mut Vec<UnresolvedReference>,
397) {
398 if !file_path.ends_with(".mbt") && !file_path.ends_with(".mbt.md") {
399 return;
400 }
401
402 let safe = strip_moonbit_comments_preserve_lines(source);
403 let call_re = Regex::new(
404 r#"@(?:sol|router)\.(route|page|api_get|api_post|api_put|api_delete|api_patch|raw_get|raw_post|raw_put|raw_delete|raw_patch)\s*\(\s*"([^"]+)"\s*,\s*([@A-Za-z_][@A-Za-z0-9_:.]*)"#,
405 )
406 .unwrap();
407 let wrap_re = Regex::new(r#"@(?:sol|router)\.wrap\s*\(\s*"([^"]*)"\s*,"#).unwrap();
408 let constructor_re = Regex::new(
409 r#"SolRoutes::(Page|RawGet|RawPost|RawPut|RawDelete|RawPatch)\s*\([^)]*path\s*=\s*"([^"]+)"[^)]*handler\s*=\s*(?:PageHandler|RawHandler)?\(?\s*([@A-Za-z_][@A-Za-z0-9_:.]*)"#,
410 )
411 .unwrap();
412 let named_page_re = Regex::new(
413 r#"@(?:sol|router)\.page\s*\([^)]*path\s*=\s*"([^"]+)"[^)]*handler\s*=\s*([@A-Za-z_][@A-Za-z0-9_:.]*)"#,
414 )
415 .unwrap();
416
417 let mut prefix_stack: Vec<(usize, String)> = Vec::new();
418 let mut byte_offset = 0usize;
419 for line in safe.lines() {
420 let indent = line.chars().take_while(|c| c.is_whitespace()).count();
421 while prefix_stack
422 .last()
423 .map(|(stack_indent, _)| indent <= *stack_indent && line.trim_start().starts_with(']'))
424 .unwrap_or(false)
425 {
426 prefix_stack.pop();
427 }
428
429 if let Some(cap) = wrap_re.captures(line) {
430 let prefix = cap.get(1).map(|m| m.as_str()).unwrap_or("");
431 let full_prefix = join_route_paths(current_route_prefix(&prefix_stack), prefix);
432 prefix_stack.push((indent, full_prefix));
433 }
434
435 for cap in call_re.captures_iter(line) {
436 let helper = cap.get(1).unwrap().as_str();
437 let path = cap.get(2).unwrap().as_str();
438 let handler = cap.get(3).map(|m| clean_moonbit_handler(m.as_str()));
439 let route_path = join_route_paths(current_route_prefix(&prefix_stack), path);
440 add_moonbit_route_node(
441 file_path,
442 &safe,
443 byte_offset + cap.get(0).unwrap().start(),
444 helper_route_method(helper),
445 &route_path,
446 handler.as_deref(),
447 now,
448 nodes,
449 edges,
450 refs,
451 );
452 }
453
454 for cap in named_page_re.captures_iter(line) {
455 let path = cap.get(1).unwrap().as_str();
456 let handler = cap.get(2).map(|m| clean_moonbit_handler(m.as_str()));
457 let route_path = join_route_paths(current_route_prefix(&prefix_stack), path);
458 add_moonbit_route_node(
459 file_path,
460 &safe,
461 byte_offset + cap.get(0).unwrap().start(),
462 "PAGE",
463 &route_path,
464 handler.as_deref(),
465 now,
466 nodes,
467 edges,
468 refs,
469 );
470 }
471
472 for cap in constructor_re.captures_iter(line) {
473 let variant = cap.get(1).unwrap().as_str();
474 let path = cap.get(2).unwrap().as_str();
475 let handler = cap.get(3).map(|m| clean_moonbit_handler(m.as_str()));
476 let route_path = join_route_paths(current_route_prefix(&prefix_stack), path);
477 add_moonbit_route_node(
478 file_path,
479 &safe,
480 byte_offset + cap.get(0).unwrap().start(),
481 constructor_route_method(variant),
482 &route_path,
483 handler.as_deref(),
484 now,
485 nodes,
486 edges,
487 refs,
488 );
489 }
490
491 byte_offset += line.len() + 1;
492 }
493}
494
495fn add_moonbit_route_node(
496 file_path: &str,
497 source: &str,
498 byte_offset: usize,
499 method: &str,
500 route_path: &str,
501 handler: Option<&str>,
502 now: i64,
503 nodes: &mut Vec<Node>,
504 edges: &mut Vec<Edge>,
505 refs: &mut Vec<UnresolvedReference>,
506) {
507 let line = line_for(source, byte_offset);
508 let name = format!("{method} {route_path}");
509 let node = Node {
510 id: format!("route:{file_path}:{line}:{method}:{route_path}"),
511 kind: NodeKind::Route,
512 name,
513 qualified_name: format!("{file_path}::route:{method}:{route_path}"),
514 file_path: file_path.to_string(),
515 language: Language::MoonBit,
516 start_line: line,
517 end_line: line,
518 start_column: 0,
519 end_column: 0,
520 docstring: None,
521 signature: handler.map(|h| format!("{method} {route_path} -> {h}")),
522 visibility: None,
523 is_exported: false,
524 is_async: false,
525 is_static: false,
526 is_abstract: false,
527 updated_at: now,
528 };
529 add_contains(nodes, edges, &node);
530 if let Some(handler) = handler {
531 refs.push(unresolved(
532 &node.id,
533 handler,
534 EdgeKind::References,
535 file_path,
536 Language::MoonBit,
537 line,
538 ));
539 }
540 nodes.push(node);
541}
542
543fn helper_route_method(helper: &str) -> &'static str {
544 match helper {
545 "route" | "page" => "PAGE",
546 "api_get" => "GET",
547 "api_post" => "POST",
548 "api_put" => "PUT",
549 "api_delete" => "DELETE",
550 "api_patch" => "PATCH",
551 "raw_get" => "RAW GET",
552 "raw_post" => "RAW POST",
553 "raw_put" => "RAW PUT",
554 "raw_delete" => "RAW DELETE",
555 "raw_patch" => "RAW PATCH",
556 _ => "PAGE",
557 }
558}
559
560fn constructor_route_method(variant: &str) -> &'static str {
561 match variant {
562 "RawGet" => "RAW GET",
563 "RawPost" => "RAW POST",
564 "RawPut" => "RAW PUT",
565 "RawDelete" => "RAW DELETE",
566 "RawPatch" => "RAW PATCH",
567 _ => "PAGE",
568 }
569}
570
571fn current_route_prefix(prefix_stack: &[(usize, String)]) -> &str {
572 prefix_stack
573 .last()
574 .map(|(_, prefix)| prefix.as_str())
575 .unwrap_or("")
576}
577
578fn join_route_paths(prefix: &str, path: &str) -> String {
579 if prefix.is_empty() || prefix == "/" {
580 return normalize_route_path(path);
581 }
582 let path = normalize_route_path(path);
583 if path == "/" {
584 return normalize_route_path(prefix);
585 }
586 format!(
587 "{}/{}",
588 prefix.trim_end_matches('/'),
589 path.trim_start_matches('/')
590 )
591}
592
593fn normalize_route_path(path: &str) -> String {
594 if path.is_empty() {
595 return "/".into();
596 }
597 let path = path.replace('\\', "/");
598 if path.starts_with('/') {
599 path
600 } else {
601 format!("/{path}")
602 }
603}
604
605fn clean_moonbit_handler(handler: &str) -> String {
606 handler
607 .trim()
608 .trim_start_matches('@')
609 .rsplit(['.', ':'])
610 .next()
611 .unwrap_or(handler)
612 .trim_matches(')')
613 .to_string()
614}
615
616fn extract_moonbit_metadata(
617 file_path: &str,
618 source: &str,
619 now: i64,
620 nodes: &mut Vec<Node>,
621 edges: &mut Vec<Edge>,
622 refs: &mut Vec<UnresolvedReference>,
623) {
624 let Ok(json) = serde_json::from_str::<serde_json::Value>(source) else {
625 return;
626 };
627 if file_path.ends_with("moon.mod.json") {
628 if let Some(name) = json.get("name").and_then(|v| v.as_str()) {
629 let node = make_node(
630 file_path,
631 Language::MoonBit,
632 NodeKind::Module,
633 name,
634 1,
635 0,
636 now,
637 Some("moon.mod.json".into()),
638 );
639 add_contains(nodes, edges, &node);
640 nodes.push(node);
641 }
642 return;
643 }
644
645 let package_name = json
646 .get("name")
647 .and_then(|v| v.as_str())
648 .or_else(|| file_path.rsplit('/').nth(1))
649 .unwrap_or("moonbit-package");
650 let node = make_node(
651 file_path,
652 Language::MoonBit,
653 NodeKind::Module,
654 package_name,
655 1,
656 0,
657 now,
658 Some(file_path.rsplit('/').next().unwrap_or("moon.pkg").into()),
659 );
660 add_contains(nodes, edges, &node);
661 let package_node_id = node.id.clone();
662 nodes.push(node);
663
664 if let Some(imports) = json.get("import").or_else(|| json.get("imports")) {
665 if let Some(obj) = imports.as_object() {
666 for (alias, value) in obj {
667 let target = value.as_str().unwrap_or(alias);
668 let import_node = make_node(
669 file_path,
670 Language::MoonBit,
671 NodeKind::Import,
672 alias,
673 1,
674 0,
675 now,
676 Some(target.to_string()),
677 );
678 add_contains(nodes, edges, &import_node);
679 refs.push(unresolved(
680 &package_node_id,
681 alias,
682 EdgeKind::Imports,
683 file_path,
684 Language::MoonBit,
685 1,
686 ));
687 nodes.push(import_node);
688 }
689 }
690 }
691}
692
693fn try_extract_rust_tree_sitter(
694 file_path: &str,
695 source: &str,
696 now: i64,
697 nodes: &mut Vec<Node>,
698 edges: &mut Vec<Edge>,
699 refs: &mut Vec<UnresolvedReference>,
700) -> bool {
701 let mut parser = Parser::new();
702 if parser
703 .set_language(&tree_sitter_rust::LANGUAGE.into())
704 .is_err()
705 {
706 return false;
707 }
708 let Some(tree) = parser.parse(source, None) else {
709 return false;
710 };
711 if tree.root_node().has_error() {
712 return false;
713 }
714
715 let root = tree.root_node();
716 let mut stack = Vec::new();
717 collect_rust_nodes(file_path, source, root, now, nodes, edges, refs, &mut stack);
718 collect_rust_refs(file_path, source, root, nodes, refs);
719 true
720}
721
722fn collect_rust_nodes(
723 file_path: &str,
724 source: &str,
725 node: SyntaxNode,
726 now: i64,
727 nodes: &mut Vec<Node>,
728 edges: &mut Vec<Edge>,
729 refs: &mut Vec<UnresolvedReference>,
730 stack: &mut Vec<String>,
731) {
732 let kind = match node.kind() {
733 "function_item" => {
734 if rust_receiver_type(node, source).is_some() {
735 Some(NodeKind::Method)
736 } else {
737 Some(NodeKind::Function)
738 }
739 }
740 "struct_item" => Some(NodeKind::Struct),
741 "trait_item" => Some(NodeKind::Trait),
742 "enum_item" => Some(NodeKind::Enum),
743 "enum_variant" => Some(NodeKind::EnumMember),
744 "type_item" => Some(NodeKind::TypeAlias),
745 "const_item" => Some(NodeKind::Constant),
746 "static_item" => Some(NodeKind::Variable),
747 "let_declaration" => Some(NodeKind::Variable),
748 "field_declaration" => Some(NodeKind::Field),
749 "function_signature_item" => Some(NodeKind::Method),
750 "use_declaration" => Some(NodeKind::Import),
751 "mod_item" => Some(NodeKind::Module),
752 _ => None,
753 };
754
755 let mut pushed = false;
756 if let Some(kind) = kind {
757 if let Some(name) = rust_node_name(node, source, kind) {
758 let signature = Some(
759 node_text(node, source)
760 .lines()
761 .next()
762 .unwrap_or("")
763 .trim()
764 .to_string(),
765 );
766 let mut out =
767 make_node_span(file_path, Language::Rust, kind, &name, node, now, signature);
768 out.is_exported = rust_is_public(node, source);
769 out.visibility = if out.is_exported {
770 Some("public".into())
771 } else if matches!(
772 kind,
773 NodeKind::Function
774 | NodeKind::Method
775 | NodeKind::Struct
776 | NodeKind::Trait
777 | NodeKind::Enum
778 | NodeKind::TypeAlias
779 ) {
780 Some("private".into())
781 } else {
782 None
783 };
784 out.is_async = node_text(node, source).trim_start().starts_with("async ")
785 || node_text(node, source).contains(" async fn ");
786 if kind == NodeKind::Method {
787 if let Some(owner) = rust_receiver_type(node, source) {
788 out.qualified_name = format!("{owner}::{name}");
789 }
790 }
791 add_contains_from_stack(nodes, edges, stack, &out, "tree-sitter");
792 let id = out.id.clone();
793 nodes.push(out);
794 if matches!(
795 kind,
796 NodeKind::Struct
797 | NodeKind::Trait
798 | NodeKind::Enum
799 | NodeKind::Module
800 | NodeKind::Function
801 | NodeKind::Method
802 ) {
803 stack.push(id);
804 pushed = true;
805 }
806 }
807 }
808
809 if node.kind() == "impl_item" {
810 if let Some((trait_name, type_name)) = rust_impl_trait_for_type(node, source) {
811 if let Some(type_node) = nodes.iter().find(|n| {
812 n.name == type_name
813 && matches!(n.kind, NodeKind::Struct | NodeKind::Enum | NodeKind::Trait)
814 }) {
815 refs_push(
816 refs,
817 &type_node.id,
818 &trait_name,
819 EdgeKind::Implements,
820 file_path,
821 Language::Rust,
822 node.start_position().row as i64 + 1,
823 node.start_position().column as i64,
824 );
825 }
826 }
827 }
828
829 for child in named_children(node) {
830 collect_rust_nodes(file_path, source, child, now, nodes, edges, refs, stack);
831 }
832
833 if pushed {
834 stack.pop();
835 }
836}
837
838fn collect_rust_refs(
839 file_path: &str,
840 source: &str,
841 node: SyntaxNode,
842 nodes: &[Node],
843 refs: &mut Vec<UnresolvedReference>,
844) {
845 match node.kind() {
846 "use_declaration" => {
847 if let Some(name) = rust_import_root(node, source) {
848 refs_push(
849 refs,
850 &format!("file:{file_path}"),
851 &name,
852 EdgeKind::Imports,
853 file_path,
854 Language::Rust,
855 node.start_position().row as i64 + 1,
856 node.start_position().column as i64,
857 );
858 }
859 }
860 "call_expression" => {
861 if let Some(function) = node.child_by_field_name("function") {
862 if let Some(name) = callable_name(function, source) {
863 if let Some(caller) =
864 enclosing_callable(nodes, node.start_position().row as i64 + 1)
865 {
866 refs_push(
867 refs,
868 &caller.id,
869 &name,
870 EdgeKind::Calls,
871 file_path,
872 Language::Rust,
873 node.start_position().row as i64 + 1,
874 node.start_position().column as i64,
875 );
876 }
877 }
878 }
879 }
880 _ => {}
881 }
882
883 for child in named_children(node) {
884 collect_rust_refs(file_path, source, child, nodes, refs);
885 }
886}
887
888fn try_extract_moonbit_tree_sitter(
889 file_path: &str,
890 source: &str,
891 now: i64,
892 nodes: &mut Vec<Node>,
893 edges: &mut Vec<Edge>,
894 refs: &mut Vec<UnresolvedReference>,
895) -> bool {
896 let mut parser = Parser::new();
897 if parser
898 .set_language(&tree_sitter_moonbit::LANGUAGE.into())
899 .is_err()
900 {
901 return false;
902 }
903 let Some(tree) = parser.parse(source, None) else {
904 return false;
905 };
906 if tree.root_node().has_error() {
907 return false;
908 }
909
910 let root = tree.root_node();
911 let mut stack = Vec::new();
912 collect_moonbit_nodes(file_path, source, root, now, nodes, edges, &mut stack);
913 collect_moonbit_refs(file_path, source, root, nodes, refs);
914 true
915}
916
917fn collect_moonbit_nodes(
918 file_path: &str,
919 source: &str,
920 node: SyntaxNode,
921 now: i64,
922 nodes: &mut Vec<Node>,
923 edges: &mut Vec<Edge>,
924 stack: &mut Vec<String>,
925) {
926 let kind = match node.kind() {
927 "function_definition" => Some(NodeKind::Function),
928 "impl_definition" => Some(NodeKind::Method),
929 "struct_definition" | "tuple_struct_definition" => Some(NodeKind::Struct),
930 "trait_definition" => Some(NodeKind::Trait),
931 "trait_method_declaration" => Some(NodeKind::Method),
932 "enum_definition" => Some(NodeKind::Enum),
933 "enum_constructor" => Some(NodeKind::EnumMember),
934 "type_alias_definition" | "type_definition" => Some(NodeKind::TypeAlias),
935 "const_definition" => Some(NodeKind::Constant),
936 "import_declaration" => Some(NodeKind::Import),
937 "package_declaration" => Some(NodeKind::Module),
938 _ => None,
939 };
940
941 let mut pushed = false;
942 if let Some(kind) = kind {
943 if let Some(name) = moonbit_node_name(node, source, kind) {
944 let signature = Some(
945 node_text(node, source)
946 .lines()
947 .next()
948 .unwrap_or("")
949 .trim()
950 .to_string(),
951 );
952 let mut out = make_node_span(
953 file_path,
954 Language::MoonBit,
955 kind,
956 &name,
957 node,
958 now,
959 signature,
960 );
961 out.is_exported = moonbit_is_public(node, source);
962 out.visibility = if out.is_exported {
963 Some("public".into())
964 } else {
965 None
966 };
967 if kind == NodeKind::Method {
968 if let Some(owner) = moonbit_impl_owner(node, source) {
969 out.qualified_name = format!("{owner}::{name}");
970 }
971 }
972 add_contains_from_stack(nodes, edges, stack, &out, "tree-sitter");
973 let id = out.id.clone();
974 nodes.push(out);
975 if matches!(
976 kind,
977 NodeKind::Struct
978 | NodeKind::Trait
979 | NodeKind::Enum
980 | NodeKind::Module
981 | NodeKind::Function
982 | NodeKind::Method
983 ) {
984 stack.push(id);
985 pushed = true;
986 }
987 }
988 }
989
990 for child in named_children(node) {
991 collect_moonbit_nodes(file_path, source, child, now, nodes, edges, stack);
992 }
993
994 if pushed {
995 stack.pop();
996 }
997}
998
999fn collect_moonbit_refs(
1000 file_path: &str,
1001 source: &str,
1002 node: SyntaxNode,
1003 nodes: &[Node],
1004 refs: &mut Vec<UnresolvedReference>,
1005) {
1006 match node.kind() {
1007 "import_declaration" => {
1008 for child in named_children(node) {
1009 if child.kind() == "import_item" {
1010 if let Some(name) = moonbit_import_name(child, source) {
1011 refs_push(
1012 refs,
1013 &format!("file:{file_path}"),
1014 &name,
1015 EdgeKind::Imports,
1016 file_path,
1017 Language::MoonBit,
1018 child.start_position().row as i64 + 1,
1019 child.start_position().column as i64,
1020 );
1021 }
1022 }
1023 }
1024 }
1025 "apply_expression" | "dot_apply_expression" | "dot_dot_apply_expression" => {
1026 if let Some(name) = moonbit_call_name(node, source) {
1027 if let Some(caller) =
1028 enclosing_callable(nodes, node.start_position().row as i64 + 1)
1029 {
1030 refs_push(
1031 refs,
1032 &caller.id,
1033 &name,
1034 EdgeKind::Calls,
1035 file_path,
1036 Language::MoonBit,
1037 node.start_position().row as i64 + 1,
1038 node.start_position().column as i64,
1039 );
1040 }
1041 }
1042 }
1043 _ => {}
1044 }
1045
1046 for child in named_children(node) {
1047 collect_moonbit_refs(file_path, source, child, nodes, refs);
1048 }
1049}
1050
1051fn extract_generic(
1052 file_path: &str,
1053 source: &str,
1054 language: Language,
1055 now: i64,
1056 nodes: &mut Vec<Node>,
1057 edges: &mut Vec<Edge>,
1058 refs: &mut Vec<UnresolvedReference>,
1059) {
1060 add_regex_nodes(
1061 file_path,
1062 source,
1063 language,
1064 now,
1065 nodes,
1066 edges,
1067 r"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)",
1068 NodeKind::Function,
1069 );
1070 add_regex_nodes(
1071 file_path,
1072 source,
1073 language,
1074 now,
1075 nodes,
1076 edges,
1077 r"(?m)^\s*(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)",
1078 NodeKind::Class,
1079 );
1080 add_call_refs(
1081 file_path,
1082 source,
1083 language,
1084 nodes,
1085 refs,
1086 r"([A-Za-z_$][A-Za-z0-9_$.]*)\s*\(",
1087 );
1088}
1089
1090fn add_regex_nodes(
1091 file_path: &str,
1092 source: &str,
1093 language: Language,
1094 now: i64,
1095 nodes: &mut Vec<Node>,
1096 edges: &mut Vec<Edge>,
1097 pattern: &str,
1098 kind: NodeKind,
1099) {
1100 let re = Regex::new(pattern).unwrap();
1101 for cap in re.captures_iter(source) {
1102 let Some(name_match) = cap.get(2).or_else(|| cap.get(1)) else {
1103 continue;
1104 };
1105 let mut name = name_match.as_str().to_string();
1106 if kind == NodeKind::Method && name.contains("::") {
1107 name = name.rsplit("::").next().unwrap_or(&name).to_string();
1108 }
1109 let signature = cap.get(0).map(|m| m.as_str().trim().to_string());
1110 let line = line_for(source, name_match.start());
1111 let mut node = make_node(file_path, language, kind, &name, line, 0, now, signature);
1112 node.is_exported = cap
1113 .get(1)
1114 .map(|m| m.as_str().contains("pub") || m.as_str().contains("export"))
1115 .unwrap_or(false);
1116 node.visibility = if node.is_exported {
1117 Some("public".into())
1118 } else {
1119 None
1120 };
1121 add_contains(nodes, edges, &node);
1122 nodes.push(node);
1123 }
1124}
1125
1126fn add_call_refs(
1127 file_path: &str,
1128 source: &str,
1129 language: Language,
1130 nodes: &[Node],
1131 refs: &mut Vec<UnresolvedReference>,
1132 pattern: &str,
1133) {
1134 let re = Regex::new(pattern).unwrap();
1135 let keywords = [
1136 "if", "for", "while", "match", "return", "fn", "test", "inspect", "Some", "Ok", "Err",
1137 ];
1138 for cap in re.captures_iter(source) {
1139 let name = cap.get(1).unwrap().as_str().rsplit("::").next().unwrap();
1140 if keywords.contains(&name) {
1141 continue;
1142 }
1143 let line = line_for(source, cap.get(1).unwrap().start());
1144 if let Some(caller) = nodes
1145 .iter()
1146 .filter(|n| matches!(n.kind, NodeKind::Function | NodeKind::Method))
1147 .rev()
1148 .find(|n| n.start_line <= line)
1149 {
1150 refs.push(unresolved(
1151 &caller.id,
1152 name,
1153 EdgeKind::Calls,
1154 file_path,
1155 language,
1156 line,
1157 ));
1158 }
1159 }
1160}
1161
1162fn make_node(
1163 file_path: &str,
1164 language: Language,
1165 kind: NodeKind,
1166 name: &str,
1167 line: i64,
1168 col: i64,
1169 now: i64,
1170 signature: Option<String>,
1171) -> Node {
1172 Node {
1173 id: format!("{}:{}:{}:{}", kind.as_str(), file_path, name, line),
1174 kind,
1175 name: name.to_string(),
1176 qualified_name: name.to_string(),
1177 file_path: file_path.to_string(),
1178 language,
1179 start_line: line,
1180 end_line: line,
1181 start_column: col,
1182 end_column: col,
1183 docstring: None,
1184 signature,
1185 visibility: None,
1186 is_exported: false,
1187 is_async: false,
1188 is_static: false,
1189 is_abstract: false,
1190 updated_at: now,
1191 }
1192}
1193
1194fn make_node_span(
1195 file_path: &str,
1196 language: Language,
1197 kind: NodeKind,
1198 name: &str,
1199 node: SyntaxNode,
1200 now: i64,
1201 signature: Option<String>,
1202) -> Node {
1203 let start = node.start_position();
1204 let end = node.end_position();
1205 Node {
1206 id: format!("{}:{}:{}:{}", kind.as_str(), file_path, name, start.row + 1),
1207 kind,
1208 name: name.to_string(),
1209 qualified_name: name.to_string(),
1210 file_path: file_path.to_string(),
1211 language,
1212 start_line: start.row as i64 + 1,
1213 end_line: end.row as i64 + 1,
1214 start_column: start.column as i64,
1215 end_column: end.column as i64,
1216 docstring: None,
1217 signature,
1218 visibility: None,
1219 is_exported: false,
1220 is_async: false,
1221 is_static: false,
1222 is_abstract: false,
1223 updated_at: now,
1224 }
1225}
1226
1227fn add_contains(nodes: &[Node], edges: &mut Vec<Edge>, node: &Node) {
1228 if let Some(file) = nodes.first() {
1229 edges.push(Edge {
1230 id: None,
1231 source: file.id.clone(),
1232 target: node.id.clone(),
1233 kind: EdgeKind::Contains,
1234 line: None,
1235 col: None,
1236 provenance: Some("regex".into()),
1237 });
1238 }
1239}
1240
1241fn add_contains_from_stack(
1242 nodes: &[Node],
1243 edges: &mut Vec<Edge>,
1244 stack: &[String],
1245 node: &Node,
1246 provenance: &str,
1247) {
1248 let source = stack
1249 .last()
1250 .cloned()
1251 .or_else(|| nodes.first().map(|n| n.id.clone()));
1252 if let Some(source) = source {
1253 edges.push(Edge {
1254 id: None,
1255 source,
1256 target: node.id.clone(),
1257 kind: EdgeKind::Contains,
1258 line: None,
1259 col: None,
1260 provenance: Some(provenance.into()),
1261 });
1262 }
1263}
1264
1265fn unresolved(
1266 from: &str,
1267 name: &str,
1268 kind: EdgeKind,
1269 file_path: &str,
1270 language: Language,
1271 line: i64,
1272) -> UnresolvedReference {
1273 UnresolvedReference {
1274 from_node_id: from.to_string(),
1275 reference_name: name.to_string(),
1276 reference_kind: kind,
1277 line,
1278 column: 0,
1279 file_path: file_path.to_string(),
1280 language,
1281 }
1282}
1283
1284fn refs_push(
1285 refs: &mut Vec<UnresolvedReference>,
1286 from: &str,
1287 name: &str,
1288 kind: EdgeKind,
1289 file_path: &str,
1290 language: Language,
1291 line: i64,
1292 column: i64,
1293) {
1294 if !name.is_empty() {
1295 refs.push(UnresolvedReference {
1296 from_node_id: from.to_string(),
1297 reference_name: name.to_string(),
1298 reference_kind: kind,
1299 line,
1300 column,
1301 file_path: file_path.to_string(),
1302 language,
1303 });
1304 }
1305}
1306
1307fn named_children(node: SyntaxNode) -> Vec<SyntaxNode> {
1308 (0..node.named_child_count())
1309 .filter_map(|i| node.named_child(i as u32))
1310 .collect()
1311}
1312
1313fn node_text<'a>(node: SyntaxNode, source: &'a str) -> &'a str {
1314 source.get(node.byte_range()).unwrap_or_default()
1315}
1316
1317fn child_text_by_kind<'a>(node: SyntaxNode, source: &'a str, kinds: &[&str]) -> Option<&'a str> {
1318 named_children(node)
1319 .into_iter()
1320 .find(|child| kinds.contains(&child.kind()))
1321 .map(|child| node_text(child, source))
1322}
1323
1324fn descendant_text_by_kind<'a>(
1325 node: SyntaxNode,
1326 source: &'a str,
1327 kinds: &[&str],
1328) -> Option<&'a str> {
1329 if kinds.contains(&node.kind()) {
1330 return Some(node_text(node, source));
1331 }
1332 for child in named_children(node) {
1333 if let Some(text) = descendant_text_by_kind(child, source, kinds) {
1334 return Some(text);
1335 }
1336 }
1337 None
1338}
1339
1340fn rust_node_name(node: SyntaxNode, source: &str, kind: NodeKind) -> Option<String> {
1341 if kind == NodeKind::Import {
1342 return rust_import_root(node, source);
1343 }
1344 if kind == NodeKind::Variable && node.kind() == "let_declaration" {
1345 return descendant_text_by_kind(node, source, &["identifier"]).map(clean_symbol_name);
1346 }
1347 if kind == NodeKind::Field {
1348 return child_text_by_kind(node, source, &["field_identifier", "identifier"])
1349 .map(clean_symbol_name);
1350 }
1351 node.child_by_field_name("name")
1352 .map(|n| clean_symbol_name(node_text(n, source)))
1353 .or_else(|| {
1354 child_text_by_kind(
1355 node,
1356 source,
1357 &["identifier", "type_identifier", "field_identifier"],
1358 )
1359 .map(clean_symbol_name)
1360 })
1361}
1362
1363fn rust_is_public(node: SyntaxNode, source: &str) -> bool {
1364 node_text(node, source).trim_start().starts_with("pub")
1365 || named_children(node).into_iter().any(|child| {
1366 child.kind() == "visibility_modifier" && node_text(child, source).contains("pub")
1367 })
1368}
1369
1370fn rust_receiver_type(node: SyntaxNode, source: &str) -> Option<String> {
1371 let mut parent = node.parent();
1372 while let Some(p) = parent {
1373 if p.kind() == "impl_item" {
1374 let mut direct = named_children(p)
1375 .into_iter()
1376 .filter(|child| {
1377 matches!(
1378 child.kind(),
1379 "type_identifier" | "generic_type" | "scoped_type_identifier"
1380 )
1381 })
1382 .collect::<Vec<_>>();
1383 if let Some(last) = direct.pop() {
1384 return Some(clean_type_name(node_text(last, source)));
1385 }
1386 return descendant_text_by_kind(p, source, &["type_identifier"]).map(clean_type_name);
1387 }
1388 parent = p.parent();
1389 }
1390 None
1391}
1392
1393fn rust_impl_trait_for_type(node: SyntaxNode, source: &str) -> Option<(String, String)> {
1394 if node.kind() != "impl_item" || !node_text(node, source).contains(" for ") {
1395 return None;
1396 }
1397 let names: Vec<String> = named_children(node)
1398 .into_iter()
1399 .filter(|child| {
1400 matches!(
1401 child.kind(),
1402 "type_identifier" | "generic_type" | "scoped_type_identifier"
1403 )
1404 })
1405 .map(|child| clean_type_name(node_text(child, source)))
1406 .collect();
1407 if names.len() >= 2 {
1408 Some((names[0].clone(), names[names.len() - 1].clone()))
1409 } else {
1410 None
1411 }
1412}
1413
1414fn rust_import_root(node: SyntaxNode, source: &str) -> Option<String> {
1415 let text = node_text(node, source)
1416 .trim()
1417 .strip_prefix("use")
1418 .unwrap_or(node_text(node, source))
1419 .trim()
1420 .trim_end_matches(';')
1421 .trim();
1422 text.split("::")
1423 .next()
1424 .map(|s| s.trim_matches('{').trim().to_string())
1425 .filter(|s| !s.is_empty())
1426}
1427
1428fn callable_name(node: SyntaxNode, source: &str) -> Option<String> {
1429 match node.kind() {
1430 "identifier" | "field_identifier" => Some(clean_symbol_name(node_text(node, source))),
1431 "scoped_identifier" => node_text(node, source)
1432 .rsplit("::")
1433 .next()
1434 .map(clean_symbol_name),
1435 "field_expression" => node
1436 .child_by_field_name("field")
1437 .map(|field| clean_symbol_name(node_text(field, source))),
1438 "generic_function" => named_children(node)
1439 .into_iter()
1440 .find_map(|child| callable_name(child, source)),
1441 _ => None,
1442 }
1443}
1444
1445fn moonbit_node_name(node: SyntaxNode, source: &str, kind: NodeKind) -> Option<String> {
1446 match kind {
1447 NodeKind::Function | NodeKind::Method => child_text_by_kind(
1448 node,
1449 source,
1450 &["function_identifier", "lowercase_identifier", "identifier"],
1451 )
1452 .map(|s| clean_symbol_name(s.rsplit("::").next().unwrap_or(s))),
1453 NodeKind::Struct | NodeKind::Trait | NodeKind::Enum => child_text_by_kind(
1454 node,
1455 source,
1456 &[
1457 "identifier",
1458 "type_identifier",
1459 "type_name",
1460 "uppercase_identifier",
1461 ],
1462 )
1463 .map(clean_symbol_name),
1464 NodeKind::EnumMember => child_text_by_kind(
1465 node,
1466 source,
1467 &["uppercase_identifier", "identifier", "type_name"],
1468 )
1469 .map(clean_symbol_name),
1470 NodeKind::TypeAlias => descendant_text_by_kind(
1471 node,
1472 source,
1473 &[
1474 "type_identifier",
1475 "type_name",
1476 "identifier",
1477 "uppercase_identifier",
1478 ],
1479 )
1480 .map(clean_symbol_name),
1481 NodeKind::Constant => {
1482 child_text_by_kind(node, source, &["uppercase_identifier", "identifier"])
1483 .map(clean_symbol_name)
1484 }
1485 NodeKind::Import => moonbit_import_name(node, source),
1486 NodeKind::Module => node
1487 .named_child(0)
1488 .map(|child| clean_quoted(node_text(child, source))),
1489 _ => None,
1490 }
1491}
1492
1493fn moonbit_is_public(node: SyntaxNode, source: &str) -> bool {
1494 named_children(node)
1495 .into_iter()
1496 .any(|child| child.kind() == "visibility" && node_text(child, source).contains("pub"))
1497 || node_text(node, source).trim_start().starts_with("pub ")
1498}
1499
1500fn moonbit_impl_owner(node: SyntaxNode, source: &str) -> Option<String> {
1501 child_text_by_kind(
1502 node,
1503 source,
1504 &["type_name", "type_identifier", "qualified_type_identifier"],
1505 )
1506 .map(clean_type_name)
1507}
1508
1509fn moonbit_import_name(node: SyntaxNode, source: &str) -> Option<String> {
1510 if node.kind() == "import_declaration" {
1511 return named_children(node)
1512 .into_iter()
1513 .find(|child| child.kind() == "import_item")
1514 .and_then(|child| moonbit_import_name(child, source));
1515 }
1516 named_children(node)
1517 .into_iter()
1518 .find(|child| child.kind() == "string_literal")
1519 .map(|child| clean_quoted(node_text(child, source)))
1520}
1521
1522fn moonbit_call_name(node: SyntaxNode, source: &str) -> Option<String> {
1523 for child in named_children(node) {
1524 match child.kind() {
1525 "qualified_identifier" | "function_identifier" | "method_expression" => {
1526 let text = node_text(child, source);
1527 let name = text
1528 .rsplit(['.', ':'])
1529 .find(|part| !part.is_empty())
1530 .unwrap_or(text);
1531 return Some(clean_symbol_name(name));
1532 }
1533 "lowercase_identifier" | "identifier" => {
1534 return Some(clean_symbol_name(node_text(child, source)));
1535 }
1536 _ => {}
1537 }
1538 }
1539 None
1540}
1541
1542fn enclosing_callable(nodes: &[Node], line: i64) -> Option<&Node> {
1543 nodes
1544 .iter()
1545 .filter(|n| matches!(n.kind, NodeKind::Function | NodeKind::Method))
1546 .filter(|n| n.start_line <= line && line <= n.end_line.max(n.start_line))
1547 .min_by_key(|n| n.end_line - n.start_line)
1548}
1549
1550fn clean_symbol_name(s: &str) -> String {
1551 s.trim()
1552 .trim_matches('"')
1553 .trim_matches('\'')
1554 .trim_start_matches('.')
1555 .to_string()
1556}
1557
1558fn clean_quoted(s: &str) -> String {
1559 s.trim().trim_matches('"').trim_matches('\'').to_string()
1560}
1561
1562fn clean_type_name(s: &str) -> String {
1563 let s = s.trim();
1564 let before_generics = s.split('<').next().unwrap_or(s);
1565 before_generics
1566 .rsplit("::")
1567 .next()
1568 .unwrap_or(before_generics)
1569 .trim()
1570 .to_string()
1571}
1572
1573fn line_for(source: &str, idx: usize) -> i64 {
1574 source[..idx.min(source.len())]
1575 .bytes()
1576 .filter(|b| *b == b'\n')
1577 .count() as i64
1578 + 1
1579}
1580
1581fn extract_mbt_markdown_code_with_padding(source: &str) -> String {
1582 let mut out = String::new();
1583 let mut in_mbt = false;
1584 for line in source.lines() {
1585 let trimmed = line.trim_start();
1586 if trimmed.starts_with("```") {
1587 in_mbt = trimmed.contains("mbt");
1588 out.push('\n');
1589 continue;
1590 }
1591 if in_mbt {
1592 out.push_str(line);
1593 }
1594 out.push('\n');
1595 }
1596 out
1597}
1598
1599fn strip_moonbit_comments_preserve_lines(source: &str) -> String {
1600 let mut out = String::with_capacity(source.len());
1601 let mut chars = source.chars().peekable();
1602 let mut in_string = false;
1603 let mut escaped = false;
1604 while let Some(ch) = chars.next() {
1605 if in_string {
1606 out.push(ch);
1607 if escaped {
1608 escaped = false;
1609 } else if ch == '\\' {
1610 escaped = true;
1611 } else if ch == '"' {
1612 in_string = false;
1613 }
1614 continue;
1615 }
1616
1617 if ch == '"' {
1618 in_string = true;
1619 out.push(ch);
1620 continue;
1621 }
1622
1623 if ch == '/' && chars.peek() == Some(&'/') {
1624 chars.next();
1625 out.push(' ');
1626 out.push(' ');
1627 for next in chars.by_ref() {
1628 if next == '\n' {
1629 out.push('\n');
1630 break;
1631 }
1632 out.push(' ');
1633 }
1634 continue;
1635 }
1636
1637 if ch == '/' && chars.peek() == Some(&'*') {
1638 chars.next();
1639 out.push(' ');
1640 out.push(' ');
1641 let mut prev = '\0';
1642 for next in chars.by_ref() {
1643 if next == '\n' {
1644 out.push('\n');
1645 } else {
1646 out.push(' ');
1647 }
1648 if prev == '*' && next == '/' {
1649 break;
1650 }
1651 prev = next;
1652 }
1653 continue;
1654 }
1655
1656 out.push(ch);
1657 }
1658 out
1659}
1660
1661fn now_ms() -> i64 {
1662 std::time::SystemTime::now()
1663 .duration_since(std::time::UNIX_EPOCH)
1664 .map(|d| d.as_millis() as i64)
1665 .unwrap_or_default()
1666}