1use anyhow::{Context, Result};
2use rmcp::schemars;
3use rustdoc_types::{Crate, Id, Item, ItemEnum};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug)]
9pub struct DocQuery {
10 crate_data: Crate,
11}
12
13#[derive(Debug, Serialize, Deserialize, JsonSchema)]
15pub struct ItemInfo {
16 pub id: String,
17 pub name: String,
18 pub kind: String,
19 pub path: Vec<String>,
20 pub docs: Option<String>,
21 pub visibility: String,
22}
23
24#[derive(Debug, Serialize, Deserialize, JsonSchema)]
26pub struct SourceLocation {
27 pub filename: String,
28 pub line_start: usize,
29 pub column_start: usize,
30 pub line_end: usize,
31 pub column_end: usize,
32}
33
34#[derive(Debug, Serialize, Deserialize, JsonSchema)]
36pub struct SourceInfo {
37 pub location: SourceLocation,
38 pub code: String,
39 pub context_lines: Option<usize>,
40}
41
42#[derive(Debug, Serialize, Deserialize, JsonSchema)]
44pub struct DetailedItem {
45 pub info: ItemInfo,
46 pub signature: Option<String>,
47 pub generics: Option<serde_json::Value>,
48 pub fields: Option<Vec<ItemInfo>>,
49 pub variants: Option<Vec<ItemInfo>>,
50 pub methods: Option<Vec<ItemInfo>>,
51 pub source_location: Option<SourceLocation>,
52}
53
54impl DocQuery {
55 pub fn new(crate_data: Crate) -> Self {
57 Self { crate_data }
58 }
59
60 pub fn list_items(&self, kind_filter: Option<&str>) -> Vec<ItemInfo> {
62 let mut items = Vec::new();
63
64 for (id, item) in &self.crate_data.index {
65 if let Some(filter) = &kind_filter
66 && self.get_item_kind_string(&item.inner) != *filter
67 {
68 continue;
69 }
70
71 if let Some(info) = self.item_to_info(id, item) {
72 items.push(info);
73 }
74 }
75
76 items.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.name.cmp(&b.name)));
78 items
79 }
80
81 pub fn search_items(&self, pattern: &str) -> Vec<ItemInfo> {
83 let pattern_lower = pattern.to_lowercase();
84 let mut items = Vec::new();
85
86 for (id, item) in &self.crate_data.index {
87 let item_name = if let Some(name) = &item.name {
89 Some(name.clone())
90 } else if let Some(path_summary) = self.crate_data.paths.get(id) {
91 path_summary.path.last().cloned()
93 } else {
94 None
95 };
96
97 if let Some(name) = item_name
98 && name.to_lowercase().contains(&pattern_lower)
99 && let Some(info) = self.item_to_info(id, item)
100 {
101 items.push(info);
102 }
103 }
104
105 items.sort_by(|a, b| {
106 let a_exact = a.name.to_lowercase() == pattern_lower;
108 let b_exact = b.name.to_lowercase() == pattern_lower;
109 let a_prefix = a.name.to_lowercase().starts_with(&pattern_lower);
110 let b_prefix = b.name.to_lowercase().starts_with(&pattern_lower);
111
112 b_exact
113 .cmp(&a_exact)
114 .then_with(|| b_prefix.cmp(&a_prefix))
115 .then_with(|| a.name.len().cmp(&b.name.len()))
116 .then_with(|| a.name.cmp(&b.name))
117 });
118
119 items
120 }
121
122 pub fn get_item_details(&self, item_id: u32) -> Result<DetailedItem> {
124 let id = Id(item_id);
125 let item = self.crate_data.index.get(&id).context("Item not found")?;
126
127 let info = self
128 .item_to_info(&id, item)
129 .context("Failed to convert item to info")?;
130
131 let mut details = DetailedItem {
132 info,
133 signature: self.get_item_signature(item),
134 generics: None,
135 fields: None,
136 variants: None,
137 methods: None,
138 source_location: self.get_item_source_location(item),
139 };
140
141 match &item.inner {
143 ItemEnum::Struct(s) => {
144 details.generics = serde_json::to_value(&s.generics).ok();
145 details.fields = Some(self.get_struct_fields(s));
146 }
147 ItemEnum::Enum(e) => {
148 details.generics = serde_json::to_value(&e.generics).ok();
149 details.variants = Some(self.get_enum_variants(e));
150 }
151 ItemEnum::Trait(t) => {
152 details.generics = serde_json::to_value(&t.generics).ok();
153 details.methods = Some(self.get_trait_items(&t.items));
154 }
155 ItemEnum::Impl(i) => {
156 details.generics = serde_json::to_value(&i.generics).ok();
157 details.methods = Some(self.get_impl_items(&i.items));
158 }
159 ItemEnum::Function(f) => {
160 details.generics = serde_json::to_value(&f.generics).ok();
161 }
162 _ => {}
163 }
164
165 Ok(details)
166 }
167
168 pub fn get_item_docs(&self, item_id: u32) -> Result<Option<String>> {
170 let id = Id(item_id);
171 let item = self.crate_data.index.get(&id).context("Item not found")?;
172
173 Ok(item.docs.clone())
174 }
175
176 fn item_to_info(&self, id: &Id, item: &Item) -> Option<ItemInfo> {
178 let name = if let Some(name) = &item.name {
180 name.clone()
181 } else if let Some(path_summary) = self.crate_data.paths.get(id) {
182 path_summary.path.last()?.clone()
183 } else {
184 return None;
185 };
186
187 let kind = self.get_item_kind_string(&item.inner);
188 let path = self.get_item_path(id);
189 let visibility = self.get_visibility_string(&item.visibility);
190
191 Some(ItemInfo {
192 id: id.0.to_string(),
193 name,
194 kind,
195 path,
196 docs: item.docs.clone(),
197 visibility,
198 })
199 }
200
201 fn get_item_kind_string(&self, inner: &ItemEnum) -> String {
203 use ItemEnum::*;
204 match inner {
205 Module(_) => "module",
206 Struct(_) => "struct",
207 Enum(_) => "enum",
208 Function(_) => "function",
209 Trait(_) => "trait",
210 Impl(_) => "impl",
211 TypeAlias(_) => "type_alias",
212 Constant { .. } => "constant",
213 Static(_) => "static",
214 Macro(_) => "macro",
215 ExternCrate { .. } => "extern_crate",
216 Use(_) => "use",
217 Union(_) => "union",
218 StructField(_) => "field",
219 Variant(_) => "variant",
220 TraitAlias(_) => "trait_alias",
221 ProcMacro(_) => "proc_macro",
222 Primitive(_) => "primitive",
223 AssocConst { .. } => "assoc_const",
224 AssocType { .. } => "assoc_type",
225 ExternType => "extern_type",
226 }
227 .to_string()
228 }
229
230 fn get_item_path(&self, id: &Id) -> Vec<String> {
232 if let Some(summary) = self.crate_data.paths.get(id) {
233 summary.path.clone()
234 } else {
235 Vec::new()
236 }
237 }
238
239 fn get_visibility_string(&self, vis: &rustdoc_types::Visibility) -> String {
241 use rustdoc_types::Visibility::*;
242 match vis {
243 Public => "public".to_string(),
244 Default => "default".to_string(),
245 Crate => "crate".to_string(),
246 Restricted { parent, .. } => format!("restricted({})", parent.0),
247 }
248 }
249
250 fn get_item_signature(&self, item: &Item) -> Option<String> {
252 use ItemEnum::*;
253 match &item.inner {
254 Function(f) => {
255 let name = item.name.as_ref()?;
256 let generics = self.format_generics(&f.generics);
257 let params = self.format_fn_params(&f.sig.inputs);
258 let output = self.format_fn_output(&f.sig.output);
259 Some(format!("fn {name}{generics}{params}{output}"))
260 }
261 _ => None,
262 }
263 }
264
265 fn format_generics(&self, generics: &rustdoc_types::Generics) -> String {
267 if generics.params.is_empty() {
269 String::new()
270 } else {
271 "<...>".to_string()
272 }
273 }
274
275 fn format_fn_params(&self, params: &[(String, rustdoc_types::Type)]) -> String {
277 let param_strs: Vec<String> = params.iter().map(|(name, _)| name.clone()).collect();
278 format!("({})", param_strs.join(", "))
279 }
280
281 fn format_fn_output(&self, output: &Option<rustdoc_types::Type>) -> String {
283 output
284 .as_ref()
285 .map(|_| " -> ...".to_string())
286 .unwrap_or_default()
287 }
288
289 fn get_struct_fields(&self, s: &rustdoc_types::Struct) -> Vec<ItemInfo> {
291 use rustdoc_types::StructKind;
292 match &s.kind {
293 StructKind::Unit => vec![],
294 StructKind::Tuple(fields) => fields
295 .iter()
296 .enumerate()
297 .filter_map(|(i, field_id)| {
298 if let Some(field_id) = field_id {
299 let item = self.crate_data.index.get(field_id)?;
300 let mut info = self.item_to_info(field_id, item)?;
301 if info.name.is_empty() {
302 info.name = i.to_string();
303 }
304 Some(info)
305 } else {
306 Some(ItemInfo {
307 id: String::new(),
308 name: format!("(field {i} stripped)"),
309 kind: "field".to_string(),
310 path: Vec::new(),
311 docs: None,
312 visibility: "private".to_string(),
313 })
314 }
315 })
316 .collect(),
317 StructKind::Plain {
318 fields,
319 has_stripped_fields,
320 } => {
321 let mut field_infos: Vec<ItemInfo> = fields
322 .iter()
323 .filter_map(|field_id| {
324 let item = self.crate_data.index.get(field_id)?;
325 self.item_to_info(field_id, item)
326 })
327 .collect();
328
329 if *has_stripped_fields {
330 field_infos.push(ItemInfo {
331 id: String::new(),
332 name: "(some fields stripped)".to_string(),
333 kind: "note".to_string(),
334 path: Vec::new(),
335 docs: None,
336 visibility: "private".to_string(),
337 });
338 }
339
340 field_infos
341 }
342 }
343 }
344
345 fn get_enum_variants(&self, e: &rustdoc_types::Enum) -> Vec<ItemInfo> {
347 let mut variant_infos: Vec<ItemInfo> = e
348 .variants
349 .iter()
350 .filter_map(|variant_id| {
351 let item = self.crate_data.index.get(variant_id)?;
352 self.item_to_info(variant_id, item)
353 })
354 .collect();
355
356 if e.has_stripped_variants {
357 variant_infos.push(ItemInfo {
358 id: String::new(),
359 name: "(some variants stripped)".to_string(),
360 kind: "note".to_string(),
361 path: Vec::new(),
362 docs: None,
363 visibility: "private".to_string(),
364 });
365 }
366
367 variant_infos
368 }
369
370 fn get_trait_items(&self, items: &[Id]) -> Vec<ItemInfo> {
372 items
373 .iter()
374 .filter_map(|item_id| {
375 let item = self.crate_data.index.get(item_id)?;
376 self.item_to_info(item_id, item)
377 })
378 .collect()
379 }
380
381 fn get_impl_items(&self, items: &[Id]) -> Vec<ItemInfo> {
383 items
384 .iter()
385 .filter_map(|item_id| {
386 let item = self.crate_data.index.get(item_id)?;
387 self.item_to_info(item_id, item)
388 })
389 .collect()
390 }
391
392 fn get_item_source_location(&self, item: &Item) -> Option<SourceLocation> {
394 let span = item.span.as_ref()?;
395 Some(SourceLocation {
396 filename: span.filename.to_string_lossy().to_string(),
397 line_start: span.begin.0,
398 column_start: span.begin.1,
399 line_end: span.end.0,
400 column_end: span.end.1,
401 })
402 }
403
404 pub fn get_item_source(
406 &self,
407 item_id: u32,
408 base_path: &std::path::Path,
409 context_lines: usize,
410 ) -> Result<SourceInfo> {
411 let id = Id(item_id);
412 let item = self.crate_data.index.get(&id).context("Item not found")?;
413
414 let span = item.span.as_ref().context("Item has no source span")?;
415 let source_path = base_path.join(&span.filename);
416
417 if !source_path.exists() {
418 anyhow::bail!("Source file not found: {}", source_path.display());
419 }
420
421 let content = std::fs::read_to_string(&source_path)
422 .with_context(|| format!("Failed to read source file: {}", source_path.display()))?;
423
424 let lines: Vec<&str> = content.lines().collect();
425
426 let start_line = span.begin.0.saturating_sub(1).saturating_sub(context_lines);
428 let end_line = std::cmp::min(span.end.0 + context_lines, lines.len());
429
430 let code_lines: Vec<String> = lines[start_line..end_line]
432 .iter()
433 .map(|line| line.to_string())
434 .collect();
435
436 Ok(SourceInfo {
437 location: SourceLocation {
438 filename: span.filename.to_string_lossy().to_string(),
439 line_start: span.begin.0,
440 column_start: span.begin.1,
441 line_end: span.end.0,
442 column_end: span.end.1,
443 },
444 code: code_lines.join("\n"),
445 context_lines: Some(context_lines),
446 })
447 }
448}