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 complexity_part = match def.metadata.lld.complexity {
57 Some(c) => format!(" · complexity={c}"),
58 None => String::new(),
59 };
60 let _ = writeln!(
61 out,
62 "**Defined in** `{}:{}-{}` · visibility={} · async={} · loc={}{}",
63 def.file.display(),
64 def.span.start_line,
65 def.span.end_line,
66 def.metadata.visibility,
67 def.metadata.is_async,
68 def.metadata.loc,
69 complexity_part,
70 );
71 if def.qualified_name != def.name {
72 let _ = writeln!(out, "**Qualified** `{}`", def.qualified_name);
73 }
74 let _ = writeln!(out);
75
76 let sig = def.metadata.definition.signature.trim();
77 if !sig.is_empty() {
78 let _ = writeln!(out, "## Signature");
79 let _ = writeln!(out, "```{lang}");
80 let _ = writeln!(out, "{sig}");
81 let _ = writeln!(out, "```");
82 let _ = writeln!(out);
83 }
84
85 if let Some(doc) = def.metadata.definition.doc_comment.as_deref() {
86 let stripped = strip_doc_markers(doc);
87 if !stripped.trim().is_empty() {
88 let _ = writeln!(out, "## Doc");
89 let _ = writeln!(out, "{}", stripped.trim());
90 let _ = writeln!(out);
91 }
92 }
93
94 write_neighbor_list(&mut out, "Callers", &ctx.callers);
95 write_neighbor_list(&mut out, "Calls", &ctx.callees);
96 write_neighbor_list(&mut out, "Used by", &ctx.used_by);
97
98 out
99}
100
101const WIKI_NEIGHBOR_LIMIT: usize = 5;
102
103fn write_neighbor_list(out: &mut String, label: &str, nodes: &[Node]) {
104 if nodes.is_empty() {
105 return;
106 }
107 let shown = nodes.len().min(WIKI_NEIGHBOR_LIMIT);
108 let _ = writeln!(out, "## {label} ({})", nodes.len());
109 for n in &nodes[..shown] {
110 let _ = writeln!(
111 out,
112 "- `{}` ({}) — `{}:{}`",
113 n.name,
114 n.kind,
115 n.file.display(),
116 n.span.start_line
117 );
118 }
119 if nodes.len() > shown {
120 let _ = writeln!(
121 out,
122 "- _+{} more — use `find_callers` for the full list_",
123 nodes.len() - shown
124 );
125 }
126 let _ = writeln!(out);
127}
128
129fn strip_doc_markers(doc: &str) -> String {
132 let mut out = String::with_capacity(doc.len());
133 for line in doc.lines() {
134 let trimmed = line.trim_start();
135 let cleaned = trimmed
136 .strip_prefix("///")
137 .or_else(|| trimmed.strip_prefix("//!"))
138 .or_else(|| trimmed.strip_prefix("/**"))
139 .or_else(|| trimmed.strip_prefix("*/"))
140 .or_else(|| trimmed.strip_prefix("//"))
141 .or_else(|| trimmed.strip_prefix("# "))
142 .or_else(|| trimmed.strip_prefix("#"))
143 .or_else(|| trimmed.strip_prefix("* "))
144 .or_else(|| trimmed.strip_prefix("*"))
145 .unwrap_or(trimmed);
146 let cleaned = cleaned
148 .trim_end()
149 .strip_suffix("*/")
150 .unwrap_or(cleaned)
151 .trim_end();
152 out.push_str(cleaned.trim_start());
153 out.push('\n');
154 }
155 out
156}
157
158fn file_lang(path: &str) -> &'static str {
160 let ext = path.rsplit('.').next().unwrap_or("");
161 match ext {
162 "rs" => "rust",
163 "py" => "python",
164 "ts" | "tsx" => "typescript",
165 "js" | "jsx" | "mjs" | "cjs" => "javascript",
166 "go" => "go",
167 "java" => "java",
168 _ => "",
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn lang_from_path() {
178 assert_eq!(file_lang("src/main.rs"), "rust");
179 assert_eq!(file_lang("app/foo.tsx"), "typescript");
180 assert_eq!(file_lang("Makefile"), "");
181 }
182
183 #[test]
184 fn strip_rust_doc_markers() {
185 let input = "/// First line\n/// Second line\n";
186 let out = strip_doc_markers(input);
187 assert!(out.contains("First line"));
188 assert!(!out.contains("///"));
189 }
190}