logicaffeine_lsp/
definition.rs1use tower_lsp::lsp_types::{GotoDefinitionResponse, Location, Position, Range, Url};
2
3use crate::document::DocumentState;
4
5pub fn goto_definition(
10 doc: &DocumentState,
11 position: Position,
12 uri: &Url,
13) -> Option<GotoDefinitionResponse> {
14 let offset = doc.line_index.offset(position);
15
16 let token = doc.tokens.iter().find(|t| {
18 offset >= t.span.start && offset < t.span.end
19 })?;
20
21 let name = doc.source.get(token.span.start..token.span.end)?;
22
23 let defs = doc.symbol_index.definitions_of(name);
25 if defs.is_empty() {
26 return None;
27 }
28
29 let locations: Vec<Location> = defs
30 .iter()
31 .filter(|d| d.span != logicaffeine_language::token::Span::default())
32 .map(|d| Location {
33 uri: uri.clone(),
34 range: Range {
35 start: doc.line_index.position(d.span.start),
36 end: doc.line_index.position(d.span.end),
37 },
38 })
39 .collect();
40
41 if locations.is_empty() {
42 None
43 } else if locations.len() == 1 {
44 Some(GotoDefinitionResponse::Scalar(locations.into_iter().next().unwrap()))
45 } else {
46 Some(GotoDefinitionResponse::Array(locations))
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::document::DocumentState;
54
55 fn make_doc(source: &str) -> DocumentState {
56 DocumentState::new(source.to_string(), 1)
57 }
58
59 fn test_uri() -> Url {
60 Url::parse("file:///test.logos").unwrap()
61 }
62
63 #[test]
64 fn goto_definition_of_variable() {
65 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
66 let pos = Position { line: 2, character: 9 };
68 let result = goto_definition(&doc, pos, &test_uri());
69 assert!(result.is_some(), "Expected definition for 'x'");
70 match result.unwrap() {
71 GotoDefinitionResponse::Scalar(loc) => {
72 assert_eq!(loc.range.start.line, 1, "Definition should be on line 1");
73 }
74 GotoDefinitionResponse::Array(locs) => {
75 assert!(!locs.is_empty(), "Expected at least one location");
76 assert_eq!(locs[0].range.start.line, 1, "Definition should be on line 1");
77 }
78 _ => panic!("Unexpected response type"),
79 }
80 }
81
82 #[test]
83 fn goto_definition_whitespace_returns_none() {
84 let doc = make_doc("## Main\n Let x be 5.\n");
85 let pos = Position { line: 0, character: 50 };
87 let result = goto_definition(&doc, pos, &test_uri());
88 assert!(result.is_none(), "Position past end of source should return None");
89 }
90
91 #[test]
92 fn goto_def_returns_none_for_keyword() {
93 let doc = make_doc("## Main\n Let x be 5.\n");
94 let pos = Position { line: 1, character: 4 };
96 let result = goto_definition(&doc, pos, &test_uri());
97 if let Some(resp) = &result {
100 match resp {
101 GotoDefinitionResponse::Scalar(loc) => {
102 assert_ne!(loc.range.start, loc.range.end, "Should have a non-empty range");
103 }
104 GotoDefinitionResponse::Array(locs) => {
105 assert!(!locs.is_empty());
106 }
107 _ => {}
108 }
109 }
110 }
111
112 #[test]
113 fn goto_def_correct_span_range() {
114 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
115 let pos = Position { line: 2, character: 9 };
116 let result = goto_definition(&doc, pos, &test_uri());
117 assert!(result.is_some(), "Expected definition for 'x'");
118 match result.unwrap() {
119 GotoDefinitionResponse::Scalar(loc) => {
120 assert_eq!(loc.range.start.line, loc.range.end.line, "Single-char name should be on same line");
122 let char_diff = loc.range.end.character - loc.range.start.character;
123 assert_eq!(char_diff, 1, "Range should span exactly 1 character for 'x', got {}", char_diff);
124 }
125 GotoDefinitionResponse::Array(locs) => {
126 let loc = &locs[0];
127 let char_diff = loc.range.end.character - loc.range.start.character;
128 assert_eq!(char_diff, 1, "Range should span exactly 1 character for 'x', got {}", char_diff);
129 }
130 _ => panic!("Unexpected response type"),
131 }
132 }
133
134 #[test]
135 fn goto_definition_returns_correct_uri() {
136 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
137 let uri = test_uri();
138 let pos = Position { line: 2, character: 9 };
139 if let Some(GotoDefinitionResponse::Scalar(loc)) = goto_definition(&doc, pos, &uri) {
140 assert_eq!(loc.uri, uri);
141 }
142 }
143}