gitcortex_mcp/mcp/
wiki.rs1use std::fmt::Write;
30
31use gitcortex_core::{
32 error::Result,
33 graph::Node,
34 store::{GraphStore, SymbolContext},
35};
36
37pub fn render_symbol<S: GraphStore + ?Sized>(
41 store: &S,
42 branch: &str,
43 name: &str,
44) -> Result<String> {
45 let ctx = store.symbol_context(branch, name)?;
46 Ok(format(ctx))
47}
48
49fn format(ctx: SymbolContext) -> String {
50 let def = &ctx.definition;
51 let lang = file_lang(&def.file.to_string_lossy());
52 let mut out = String::with_capacity(1024);
53
54 let _ = writeln!(out, "# {} ({})", def.name, def.kind);
55 let _ = writeln!(out);
56 let _ = writeln!(
57 out,
58 "**Defined in** `{}:{}-{}` · visibility={} · async={} · loc={}",
59 def.file.display(),
60 def.span.start_line,
61 def.span.end_line,
62 def.metadata.visibility,
63 def.metadata.is_async,
64 def.metadata.loc,
65 );
66 if def.qualified_name != def.name {
67 let _ = writeln!(out, "**Qualified** `{}`", def.qualified_name);
68 }
69 let _ = writeln!(out);
70
71 let sig = def.metadata.definition.signature.trim();
72 if !sig.is_empty() {
73 let _ = writeln!(out, "## Signature");
74 let _ = writeln!(out, "```{lang}");
75 let _ = writeln!(out, "{sig}");
76 let _ = writeln!(out, "```");
77 let _ = writeln!(out);
78 }
79
80 if let Some(doc) = def.metadata.definition.doc_comment.as_deref() {
81 let stripped = strip_doc_markers(doc);
82 if !stripped.trim().is_empty() {
83 let _ = writeln!(out, "## Doc");
84 let _ = writeln!(out, "{}", stripped.trim());
85 let _ = writeln!(out);
86 }
87 }
88
89 write_neighbor_list(&mut out, "Callers", &ctx.callers);
90 write_neighbor_list(&mut out, "Calls", &ctx.callees);
91 write_neighbor_list(&mut out, "Used by", &ctx.used_by);
92
93 out
94}
95
96const WIKI_NEIGHBOR_LIMIT: usize = 5;
97
98fn write_neighbor_list(out: &mut String, label: &str, nodes: &[Node]) {
99 if nodes.is_empty() {
100 return;
101 }
102 let shown = nodes.len().min(WIKI_NEIGHBOR_LIMIT);
103 let _ = writeln!(out, "## {label} ({})", nodes.len());
104 for n in &nodes[..shown] {
105 let _ = writeln!(
106 out,
107 "- `{}` ({}) — `{}:{}`",
108 n.name,
109 n.kind,
110 n.file.display(),
111 n.span.start_line
112 );
113 }
114 if nodes.len() > shown {
115 let _ = writeln!(
116 out,
117 "- _+{} more — use `find_callers` for the full list_",
118 nodes.len() - shown
119 );
120 }
121 let _ = writeln!(out);
122}
123
124fn strip_doc_markers(doc: &str) -> String {
127 let mut out = String::with_capacity(doc.len());
128 for line in doc.lines() {
129 let trimmed = line.trim_start();
130 let cleaned = trimmed
131 .strip_prefix("///")
132 .or_else(|| trimmed.strip_prefix("//!"))
133 .or_else(|| trimmed.strip_prefix("/**"))
134 .or_else(|| trimmed.strip_prefix("*/"))
135 .or_else(|| trimmed.strip_prefix("//"))
136 .or_else(|| trimmed.strip_prefix("# "))
137 .or_else(|| trimmed.strip_prefix("#"))
138 .or_else(|| trimmed.strip_prefix("* "))
139 .or_else(|| trimmed.strip_prefix("*"))
140 .unwrap_or(trimmed);
141 let cleaned = cleaned
143 .trim_end()
144 .strip_suffix("*/")
145 .unwrap_or(cleaned)
146 .trim_end();
147 out.push_str(cleaned.trim_start());
148 out.push('\n');
149 }
150 out
151}
152
153fn file_lang(path: &str) -> &'static str {
155 let ext = path.rsplit('.').next().unwrap_or("");
156 match ext {
157 "rs" => "rust",
158 "py" => "python",
159 "ts" | "tsx" => "typescript",
160 "js" | "jsx" | "mjs" | "cjs" => "javascript",
161 "go" => "go",
162 "java" => "java",
163 _ => "",
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn lang_from_path() {
173 assert_eq!(file_lang("src/main.rs"), "rust");
174 assert_eq!(file_lang("app/foo.tsx"), "typescript");
175 assert_eq!(file_lang("Makefile"), "");
176 }
177
178 #[test]
179 fn strip_rust_doc_markers() {
180 let input = "/// First line\n/// Second line\n";
181 let out = strip_doc_markers(input);
182 assert!(out.contains("First line"));
183 assert!(!out.contains("///"));
184 }
185}