use tower_lsp::lsp_types::{GotoDefinitionResponse, Location, Position, Range, Url};
use crate::document::DocumentState;
pub fn goto_definition(
doc: &DocumentState,
position: Position,
uri: &Url,
) -> Option<GotoDefinitionResponse> {
let offset = doc.line_index.offset(position);
let token = doc.tokens.iter().find(|t| {
offset >= t.span.start && offset < t.span.end
})?;
let name = doc.source.get(token.span.start..token.span.end)?;
let defs = doc.symbol_index.definitions_of(name);
if defs.is_empty() {
return None;
}
let locations: Vec<Location> = defs
.iter()
.filter(|d| d.span != logicaffeine_language::token::Span::default())
.map(|d| Location {
uri: uri.clone(),
range: Range {
start: doc.line_index.position(d.span.start),
end: doc.line_index.position(d.span.end),
},
})
.collect();
if locations.is_empty() {
None
} else if locations.len() == 1 {
Some(GotoDefinitionResponse::Scalar(locations.into_iter().next().unwrap()))
} else {
Some(GotoDefinitionResponse::Array(locations))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::DocumentState;
fn make_doc(source: &str) -> DocumentState {
DocumentState::new(source.to_string(), 1)
}
fn test_uri() -> Url {
Url::parse("file:///test.logos").unwrap()
}
#[test]
fn goto_definition_of_variable() {
let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
let pos = Position { line: 2, character: 9 };
let result = goto_definition(&doc, pos, &test_uri());
assert!(result.is_some(), "Expected definition for 'x'");
match result.unwrap() {
GotoDefinitionResponse::Scalar(loc) => {
assert_eq!(loc.range.start.line, 1, "Definition should be on line 1");
}
GotoDefinitionResponse::Array(locs) => {
assert!(!locs.is_empty(), "Expected at least one location");
assert_eq!(locs[0].range.start.line, 1, "Definition should be on line 1");
}
_ => panic!("Unexpected response type"),
}
}
#[test]
fn goto_definition_whitespace_returns_none() {
let doc = make_doc("## Main\n Let x be 5.\n");
let pos = Position { line: 0, character: 50 };
let result = goto_definition(&doc, pos, &test_uri());
assert!(result.is_none(), "Position past end of source should return None");
}
#[test]
fn goto_def_returns_none_for_keyword() {
let doc = make_doc("## Main\n Let x be 5.\n");
let pos = Position { line: 1, character: 4 };
let result = goto_definition(&doc, pos, &test_uri());
if let Some(resp) = &result {
match resp {
GotoDefinitionResponse::Scalar(loc) => {
assert_ne!(loc.range.start, loc.range.end, "Should have a non-empty range");
}
GotoDefinitionResponse::Array(locs) => {
assert!(!locs.is_empty());
}
_ => {}
}
}
}
#[test]
fn goto_def_correct_span_range() {
let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
let pos = Position { line: 2, character: 9 };
let result = goto_definition(&doc, pos, &test_uri());
assert!(result.is_some(), "Expected definition for 'x'");
match result.unwrap() {
GotoDefinitionResponse::Scalar(loc) => {
assert_eq!(loc.range.start.line, loc.range.end.line, "Single-char name should be on same line");
let char_diff = loc.range.end.character - loc.range.start.character;
assert_eq!(char_diff, 1, "Range should span exactly 1 character for 'x', got {}", char_diff);
}
GotoDefinitionResponse::Array(locs) => {
let loc = &locs[0];
let char_diff = loc.range.end.character - loc.range.start.character;
assert_eq!(char_diff, 1, "Range should span exactly 1 character for 'x', got {}", char_diff);
}
_ => panic!("Unexpected response type"),
}
}
#[test]
fn goto_definition_returns_correct_uri() {
let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
let uri = test_uri();
let pos = Position { line: 2, character: 9 };
if let Some(GotoDefinitionResponse::Scalar(loc)) = goto_definition(&doc, pos, &uri) {
assert_eq!(loc.uri, uri);
}
}
}