use rmcp::ErrorData as McpError;
use rmcp::model::CallToolResult;
use super::ServerState;
use super::helpers::json_result;
use super::types::{DefinitionLocation, GotoDefinitionParams, GotoDefinitionResponse};
use crate::intel::model::ResolvedEdge;
use crate::path::RelPath;
pub(super) async fn run_goto_definition(
state: &ServerState,
params: GotoDefinitionParams,
) -> Result<CallToolResult, McpError> {
let abs = state.root.join(params.path.to_path_buf());
let source = std::fs::read(&abs)
.map_err(|e| McpError::invalid_params(format!("goto_definition: read {}: {e}", params.path), None))?;
let pos = line_col_to_offset(&source, params.line, params.column).ok_or_else(|| {
McpError::invalid_params(
format!(
"goto_definition: position {}:{} is out of range in {}",
params.line, params.column, params.path
),
None,
)
})?;
let resolved: Option<(RelPath, u32)> = {
let store = state.store.read().await;
let Some(entry) = store.lookup(¶ms.path) else {
return Err(McpError::invalid_params(
format!("goto_definition: {} is not indexed", params.path),
None,
));
};
let refs = store.read_resolved_by_hex(&entry.hash_hex).unwrap_or_else(|error| {
tracing::debug!(path = %params.path, %error, "goto_definition: resolution blob unreadable — treating as no in-file edge");
None
});
let intra_def = refs
.as_ref()
.and_then(|r| containing_edge(&r.intra, pos))
.map(|e| e.def_start);
match intra_def {
Some(def_start) => Some(
crate::query::definition_of(&store, ¶ms.path, def_start)
.unwrap_or((params.path.clone(), def_start)),
),
None => crate::query::definition_of(&store, ¶ms.path, pos),
}
};
let definition = resolved
.and_then(|(def_path, def_start)| definition_location(state, ¶ms.path, &source, def_path, def_start));
let (line, column) = offset_to_line_col(&source, pos);
json_result(&GotoDefinitionResponse {
path: params.path,
line,
column,
definition,
})
}
fn definition_location(
state: &ServerState,
query_path: &RelPath,
query_source: &[u8],
def_path: RelPath,
def_start: u32,
) -> Option<DefinitionLocation> {
if &def_path == query_path {
let (line, column) = offset_to_line_col(query_source, def_start);
let name = identifier_at(query_source, def_start);
return Some(DefinitionLocation {
path: def_path,
line,
column,
name,
});
}
let abs = state.root.join(def_path.to_path_buf());
let def_source = std::fs::read(&abs).ok()?;
let (line, column) = offset_to_line_col(&def_source, def_start);
let name = identifier_at(&def_source, def_start);
Some(DefinitionLocation {
path: def_path,
line,
column,
name,
})
}
fn containing_edge(edges: &[ResolvedEdge], pos: u32) -> Option<&ResolvedEdge> {
edges
.iter()
.find(|e| pos >= e.use_start && pos < e.use_end.max(e.use_start + 1))
}
fn line_col_to_offset(source: &[u8], line: u32, column: u32) -> Option<u32> {
if line == 0 {
return None;
}
let mut line_start = 0usize;
let mut cursor = 0usize;
let mut current = 1u32;
while current < line {
let rel = memchr::memchr(b'\n', &source[cursor..])?;
cursor += rel + 1;
line_start = cursor;
current += 1;
}
let line_end = match memchr::memchr(b'\n', &source[line_start..]) {
Some(rel) => line_start + rel,
None => source.len(),
};
let offset = line_start + column as usize;
(offset <= line_end).then_some(offset as u32)
}
pub(super) fn offset_to_line_col(source: &[u8], offset: u32) -> (u32, u32) {
let offset = (offset as usize).min(source.len());
let before = &source[..offset];
let line = 1 + memchr::memchr_iter(b'\n', before).count() as u32;
let line_start = memchr::memrchr(b'\n', before).map_or(0, |p| p + 1);
(line, (offset - line_start) as u32)
}
pub(super) fn identifier_at(source: &[u8], start: u32) -> String {
let start = start as usize;
let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'$';
if start >= source.len() || !is_ident(source[start]) {
return String::new();
}
let mut end = start;
while end < source.len() && is_ident(source[end]) {
end += 1;
}
String::from_utf8_lossy(&source[start..end]).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_col_to_offset_maps_positions() {
let src = b"const count = 1;\nreturn count;\n";
assert_eq!(line_col_to_offset(src, 1, 6), Some(6));
let second = (src.iter().position(|&b| b == b'\n').unwrap() + 1 + "return ".len()) as u32;
assert_eq!(line_col_to_offset(src, 2, 7), Some(second));
assert_eq!(line_col_to_offset(src, 9, 0), None);
assert_eq!(
line_col_to_offset(src, 1, 17),
None,
"col past EOL must be None, not next line"
);
assert_eq!(line_col_to_offset(src, 1, 16), Some(16));
}
#[test]
fn identifier_at_reads_the_token() {
let src = b"const count = 1;";
assert_eq!(identifier_at(src, 6), "count");
assert_eq!(identifier_at(b"let a_1$ = 2;", 4), "a_1$");
assert_eq!(identifier_at(src, 5), "");
assert_eq!(identifier_at(src, 999), "");
}
#[test]
fn offset_to_line_col_roundtrips() {
let src = b"a\nbb\nccc";
assert_eq!(offset_to_line_col(src, 0), (1, 0));
assert_eq!(offset_to_line_col(src, 2), (2, 0));
assert_eq!(offset_to_line_col(src, 5), (3, 0));
assert_eq!(offset_to_line_col(src, 999), (3, 3));
}
#[test]
fn containing_edge_respects_spans() {
let zero_width = ResolvedEdge {
use_start: 10,
use_end: 10,
def_start: 0,
def_end: 0,
};
assert!(containing_edge(std::slice::from_ref(&zero_width), 10).is_some());
assert!(containing_edge(std::slice::from_ref(&zero_width), 11).is_none());
let spanned = ResolvedEdge {
use_start: 20,
use_end: 25,
def_start: 0,
def_end: 5,
};
assert!(containing_edge(std::slice::from_ref(&spanned), 20).is_some());
assert!(containing_edge(std::slice::from_ref(&spanned), 24).is_some());
assert!(containing_edge(std::slice::from_ref(&spanned), 25).is_none());
}
}