use std::ops::Bound;
use rmcp::ErrorData as McpError;
use rmcp::model::CallToolResult;
use super::cursor::{Cursor, prefix_upper_bound};
use super::helpers::{
SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, json_result, kind_to_str, parse_kind,
};
use super::types::ReferenceHit;
pub(super) fn resolve_call_line_col(
idx: &crate::index::IndexDb,
rel: &crate::path::RelPath,
start_byte: u32,
) -> (u32, u32) {
let key = crate::index::keys::call_by_path(rel, start_byte);
let value = match idx.calls_by_path.get(key) {
Ok(Some(v)) => v,
_ => return (0, 0),
};
let call: crate::extract::Call = match rmp_serde::from_slice(&value) {
Ok(c) => c,
Err(_) => return (0, 0),
};
(call.start_row + 1, call.start_col)
}
pub(super) fn run_find_references(
idx: Option<&crate::index::IndexDb>,
params: super::types::FindReferencesParams,
) -> Result<CallToolResult, McpError> {
use super::types::FindReferencesResponse;
let limit = params
.limit
.unwrap_or(SEARCH_LIMIT_DEFAULT)
.min(SEARCH_LIMIT_MAX) as usize;
let Some(idx) = idx else {
return json_result(&FindReferencesResponse {
name: params.name,
total: 0,
total_is_partial: false,
hits: Vec::new(),
next_cursor: None,
});
};
let cursor_bytes = params
.cursor
.as_ref()
.map(|c| c.decode_fjall())
.transpose()?;
let scan = scan_calls_by_name(idx, ¶ms.name, limit, cursor_bytes.as_deref())?;
json_result(&FindReferencesResponse {
name: params.name,
total: scan.total,
total_is_partial: scan.total_is_partial,
hits: scan.hits,
next_cursor: scan.next_cursor,
})
}
pub(super) fn run_find_callers(
idx: Option<&crate::index::IndexDb>,
params: super::types::FindCallersParams,
cache: &super::MapCache,
) -> Result<CallToolResult, McpError> {
use super::types::{DefinitionView, FindCallersResponse};
let limit = params
.limit
.unwrap_or(SEARCH_LIMIT_DEFAULT)
.min(SEARCH_LIMIT_MAX) as usize;
let kind_filter = params.kind.as_deref().map(parse_kind).transpose()?;
let Some(idx) = idx else {
return json_result(&FindCallersResponse {
definition: None,
total: 0,
total_is_partial: false,
hits: Vec::new(),
next_cursor: None,
});
};
let definition: Option<DefinitionView> = cache
.by_path
.get(¶ms.path)
.and_then(|l1| {
l1.symbols
.iter()
.find(|s| s.name == params.name && kind_filter.is_none_or(|k| s.kind == k))
})
.map(|sym| DefinitionView {
path: params.path.clone(),
name: sym.name.clone(),
kind: kind_to_str(sym.kind),
start_row: sym.start_row,
start_col: sym.start_col,
});
let cursor_bytes = params
.cursor
.as_ref()
.map(|c| c.decode_fjall())
.transpose()?;
let scan = scan_calls_by_name(idx, ¶ms.name, limit, cursor_bytes.as_deref())?;
json_result(&FindCallersResponse {
definition,
total: scan.total,
total_is_partial: scan.total_is_partial,
hits: scan.hits,
next_cursor: scan.next_cursor,
})
}
pub(super) struct CallScanPage {
pub total: u32,
pub total_is_partial: bool,
pub hits: Vec<ReferenceHit>,
pub next_cursor: Option<Cursor>,
}
fn scan_calls_by_name(
idx: &crate::index::IndexDb,
name: &str,
limit: usize,
cursor_after: Option<&[u8]>,
) -> Result<CallScanPage, McpError> {
let prefix = crate::index::keys::calls_by_callee_prefix(name);
let upper = prefix_upper_bound(&prefix);
let lower = match cursor_after {
Some(k) => Bound::Excluded(k.to_vec()),
None => Bound::Included(prefix.clone()),
};
let upper_bound: Bound<Vec<u8>> = match upper {
Some(b) => Bound::Excluded(b),
None => Bound::Unbounded,
};
let mut hits: Vec<ReferenceHit> = Vec::with_capacity(limit.min(64));
let mut total: u32 = 0;
let mut total_is_partial = false;
let scan_cap = limit.saturating_mul(8).max(2_000);
let mut last_emitted_key: Option<Vec<u8>> = None;
let mut has_more = false;
for guard in idx
.calls_by_callee
.range::<Vec<u8>, _>((lower, upper_bound))
{
let (k, _) = guard
.into_inner()
.map_err(|e| McpError::internal_error(format!("index iter: {e}"), None))?;
let Some((callee, rel, start)) = crate::index::keys::parse_call_by_callee(&k) else {
continue;
};
total += 1;
if hits.len() < limit {
let (line, column) = resolve_call_line_col(idx, &rel, start);
hits.push(ReferenceHit {
path: rel,
line,
column,
callee,
});
last_emitted_key = Some(k.to_vec());
} else {
has_more = true;
}
if total as usize >= scan_cap {
total_is_partial = true;
break;
}
}
let next_cursor = if has_more {
last_emitted_key.as_deref().map(Cursor::encode_fjall)
} else {
None
};
Ok(CallScanPage {
total,
total_is_partial,
hits,
next_cursor,
})
}