1use std::collections::HashMap;
18use std::path::PathBuf;
19
20use bock_air::{resolve_names_with_registry, ModuleRegistry, NameKind, SymbolTable};
21use bock_ast::{
22 visitor::{
23 walk_class_decl, walk_effect_decl, walk_enum_decl, walk_expr, walk_fn_decl,
24 walk_impl_block, walk_module, walk_record_decl, walk_trait_decl, walk_type_expr, Visitor,
25 },
26 ClassDecl, ConstDecl, EffectDecl, EnumDecl, EnumVariant, Expr, FnDecl, ImplBlock, Item, Module,
27 NodeId, Param, Pattern, RecordDecl, RecordPatternField, TraitDecl, TypeAliasDecl, TypeExpr,
28 TypePath,
29};
30use bock_errors::{FileId, Span};
31use bock_lexer::Lexer;
32use bock_parser::Parser;
33use bock_source::SourceMap;
34
35pub struct DefinitionResult {
37 pub source_map: SourceMap,
40 pub file_id: FileId,
43 pub target: Span,
45}
46
47#[must_use]
51pub fn find_definition(
52 path: PathBuf,
53 content: String,
54 line: u32,
55 character: u32,
56) -> Option<DefinitionResult> {
57 let mut source_map = SourceMap::new();
58 let file_id = source_map.add_file(path, content);
59 let source_file = source_map.get_file(file_id);
60
61 let offset = position_to_offset(&source_file.content, line, character)?;
62
63 let mut lexer = Lexer::new(source_file);
66 let tokens = lexer.tokenize();
67 let mut parser = Parser::new(tokens, source_file);
68 let module = parser.parse_module();
69
70 let registry = ModuleRegistry::new();
72 let mut symbols = SymbolTable::new();
73 let _ = resolve_names_with_registry(&module, &mut symbols, ®istry);
74
75 let mut finder = DefinitionFinder::new(offset);
78 finder.collect_toplevel_names(&module);
79 finder.visit_module(&module);
80
81 let target = finder
84 .identifier_id
85 .and_then(|id| symbols.resolutions.get(&id))
86 .filter(|r| r.kind != NameKind::Builtin)
87 .and_then(|r| finder.def_spans.get(&r.def_id).copied())
88 .or_else(|| {
89 finder
90 .type_ref_name
91 .as_ref()
92 .and_then(|n| finder.toplevel_by_name.get(n).copied())
93 })?;
94
95 Some(DefinitionResult {
96 source_map,
97 file_id,
98 target,
99 })
100}
101
102#[must_use]
109pub fn position_to_offset(content: &str, line: u32, character: u32) -> Option<usize> {
110 let mut line_start = 0usize;
112 if line > 0 {
113 let mut seen = 0u32;
114 let mut found = false;
115 for (i, ch) in content.char_indices() {
116 if ch == '\n' {
117 seen += 1;
118 if seen == line {
119 line_start = i + 1;
120 found = true;
121 break;
122 }
123 }
124 }
125 if !found {
126 return None;
127 }
128 }
129
130 let rest = &content[line_start..];
133 let mut byte_offset = 0usize;
134 for (counted, (i, ch)) in rest.char_indices().enumerate() {
135 if counted as u32 == character {
136 return Some(line_start + i);
137 }
138 if ch == '\n' {
139 return Some(line_start + i);
140 }
141 byte_offset = i + ch.len_utf8();
142 }
143 Some(line_start + byte_offset)
144}
145
146struct DefinitionFinder {
153 offset: usize,
154 def_spans: HashMap<NodeId, Span>,
155 toplevel_by_name: HashMap<String, Span>,
160 identifier_id: Option<NodeId>,
161 best_width: usize,
164 type_ref_name: Option<String>,
166 best_type_width: usize,
168}
169
170impl DefinitionFinder {
171 fn new(offset: usize) -> Self {
172 Self {
173 offset,
174 def_spans: HashMap::new(),
175 toplevel_by_name: HashMap::new(),
176 identifier_id: None,
177 best_width: usize::MAX,
178 type_ref_name: None,
179 best_type_width: usize::MAX,
180 }
181 }
182
183 fn span_contains(&self, span: Span) -> bool {
184 self.offset >= span.start && self.offset <= span.end
185 }
186
187 fn record_decl(&mut self, id: NodeId, span: Span) {
188 self.def_spans.insert(id, span);
189 }
190
191 fn collect_toplevel_names(&mut self, module: &Module) {
194 for item in &module.items {
195 match item {
196 Item::Fn(d) => {
197 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
198 }
199 Item::Record(d) => {
200 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
201 }
202 Item::Enum(d) => {
203 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
204 for v in &d.variants {
205 let (name, span) = match v {
206 EnumVariant::Unit { name, .. }
207 | EnumVariant::Struct { name, .. }
208 | EnumVariant::Tuple { name, .. } => (name.name.clone(), name.span),
209 };
210 self.toplevel_by_name.insert(name, span);
211 }
212 }
213 Item::Class(d) => {
214 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
215 }
216 Item::Trait(d) | Item::PlatformTrait(d) => {
217 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
218 }
219 Item::Effect(d) => {
220 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
221 }
222 Item::TypeAlias(d) => {
223 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
224 }
225 Item::Const(d) => {
226 self.toplevel_by_name.insert(d.name.name.clone(), d.name.span);
227 }
228 Item::Impl(_)
229 | Item::ModuleHandle(_)
230 | Item::PropertyTest(_)
231 | Item::Error { .. } => {}
232 }
233 }
234 }
235
236 fn probe_type_path(&mut self, path: &TypePath) {
239 for seg in &path.segments {
240 if self.span_contains(seg.span) {
241 let width = seg.span.end.saturating_sub(seg.span.start);
242 if width < self.best_type_width {
243 self.best_type_width = width;
244 self.type_ref_name = Some(seg.name.clone());
245 }
246 }
247 }
248 }
249
250 fn record_pattern_bindings(&mut self, pattern: &Pattern) {
251 match pattern {
252 Pattern::Bind { id, span, .. } | Pattern::MutBind { id, span, .. } => {
253 self.record_decl(*id, *span);
254 }
255 Pattern::Tuple { elems, .. } => {
256 for e in elems {
257 self.record_pattern_bindings(e);
258 }
259 }
260 Pattern::Constructor { fields, .. } => {
261 for f in fields {
262 self.record_pattern_bindings(f);
263 }
264 }
265 Pattern::Record { fields, .. } => {
266 for RecordPatternField { pattern, .. } in fields {
267 if let Some(p) = pattern {
268 self.record_pattern_bindings(p);
269 }
270 }
274 }
275 Pattern::List { elems, rest, .. } => {
276 for e in elems {
277 self.record_pattern_bindings(e);
278 }
279 if let Some(r) = rest {
280 self.record_pattern_bindings(r);
281 }
282 }
283 Pattern::Or { alternatives, .. } => {
284 if let Some(first) = alternatives.first() {
285 self.record_pattern_bindings(first);
286 }
287 }
288 Pattern::Range { lo, hi, .. } => {
289 self.record_pattern_bindings(lo);
290 self.record_pattern_bindings(hi);
291 }
292 Pattern::Wildcard { .. } | Pattern::Literal { .. } | Pattern::Rest { .. } => {}
293 }
294 }
295}
296
297impl Visitor for DefinitionFinder {
298 fn visit_module(&mut self, node: &Module) {
299 walk_module(self, node);
300 }
301
302 fn visit_fn_decl(&mut self, node: &FnDecl) {
303 self.record_decl(node.id, node.name.span);
304 walk_fn_decl(self, node);
305 }
306
307 fn visit_record_decl(&mut self, node: &RecordDecl) {
308 self.record_decl(node.id, node.name.span);
309 for f in &node.fields {
310 self.record_decl(f.id, f.name.span);
311 }
312 walk_record_decl(self, node);
313 }
314
315 fn visit_enum_decl(&mut self, node: &EnumDecl) {
316 self.record_decl(node.id, node.name.span);
317 for v in &node.variants {
318 match v {
319 EnumVariant::Unit { id, name, .. }
320 | EnumVariant::Struct { id, name, .. }
321 | EnumVariant::Tuple { id, name, .. } => {
322 self.record_decl(*id, name.span);
323 }
324 }
325 }
326 walk_enum_decl(self, node);
327 }
328
329 fn visit_class_decl(&mut self, node: &ClassDecl) {
330 self.record_decl(node.id, node.name.span);
331 for f in &node.fields {
332 self.record_decl(f.id, f.name.span);
333 }
334 walk_class_decl(self, node);
335 }
336
337 fn visit_trait_decl(&mut self, node: &TraitDecl) {
338 self.record_decl(node.id, node.name.span);
339 walk_trait_decl(self, node);
340 }
341
342 fn visit_effect_decl(&mut self, node: &EffectDecl) {
343 self.record_decl(node.id, node.name.span);
344 walk_effect_decl(self, node);
345 }
346
347 fn visit_impl_block(&mut self, node: &ImplBlock) {
348 walk_impl_block(self, node);
349 }
350
351 fn visit_type_alias_decl(&mut self, node: &TypeAliasDecl) {
352 self.record_decl(node.id, node.name.span);
353 }
354
355 fn visit_const_decl(&mut self, node: &ConstDecl) {
356 self.record_decl(node.id, node.name.span);
357 self.visit_expr(&node.value);
360 }
361
362 fn visit_param(&mut self, node: &Param) {
363 self.record_pattern_bindings(&node.pattern);
364 if let Some(default) = &node.default {
365 self.visit_expr(default);
366 }
367 }
368
369 fn visit_pattern(&mut self, node: &Pattern) {
370 self.record_pattern_bindings(node);
371 }
372
373 fn visit_expr(&mut self, node: &Expr) {
374 match node {
375 Expr::Identifier { id, span, .. } => {
376 if self.span_contains(*span) {
377 let width = span.end.saturating_sub(span.start);
378 if width < self.best_width {
379 self.best_width = width;
380 self.identifier_id = Some(*id);
381 }
382 }
383 }
385 Expr::RecordConstruct { path, .. } => {
386 self.probe_type_path(path);
387 walk_expr(self, node);
388 }
389 _ => walk_expr(self, node),
390 }
391 }
392
393 fn visit_type_expr(&mut self, node: &TypeExpr) {
394 if let TypeExpr::Named { path, .. } = node {
395 self.probe_type_path(path);
396 }
397 walk_type_expr(self, node);
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn offset(content: &str, line: u32, ch: u32) -> usize {
406 position_to_offset(content, line, ch).expect("position valid")
407 }
408
409 #[test]
410 fn position_to_offset_start_of_file() {
411 assert_eq!(offset("hello\nworld", 0, 0), 0);
412 }
413
414 #[test]
415 fn position_to_offset_mid_first_line() {
416 assert_eq!(offset("hello\nworld", 0, 3), 3);
417 }
418
419 #[test]
420 fn position_to_offset_second_line() {
421 assert_eq!(offset("hello\nworld", 1, 2), 8); }
423
424 #[test]
425 fn position_to_offset_clamps_past_eol() {
426 assert_eq!(offset("ab\ncd", 0, 99), 2);
429 }
430
431 #[test]
432 fn position_to_offset_unknown_line_returns_none() {
433 assert!(position_to_offset("only one line", 5, 0).is_none());
434 }
435
436 #[test]
437 fn position_to_offset_multibyte() {
438 assert_eq!(offset("aéx", 0, 2), 3);
441 }
442
443 #[test]
444 fn definition_finds_fn_declaration_from_call_site() {
445 let src = "\
446module m
447
448public fn greet(name: String) -> String {
449 name
450}
451
452fn caller() -> String {
453 greet(\"world\")
454}
455";
456 let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 7, 4)
458 .expect("definition found");
459 let source = result.source_map.get_file(result.file_id);
461 let (line, _) = source.line_col(result.target.start);
462 assert_eq!(line, 3, "target should point at the fn declaration line");
463 assert_eq!(source.slice(result.target), "greet");
464 }
465
466 #[test]
467 fn definition_finds_let_binding_from_use_site() {
468 let src = "\
469module m
470
471fn main() {
472 let answer = 42
473 answer
474}
475";
476 let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 4, 4)
478 .expect("definition found");
479 let source = result.source_map.get_file(result.file_id);
480 assert_eq!(source.slice(result.target), "answer");
481 }
482
483 #[test]
484 fn definition_returns_none_for_unresolved_name() {
485 let src = "\
486module m
487
488fn main() {
489 undefined_name
490}
491";
492 assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 4).is_none());
494 }
495
496 #[test]
497 fn definition_returns_none_for_builtin() {
498 let src = "\
499module m
500
501fn main() {
502 print(\"hi\")
503}
504";
505 assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 4).is_none());
507 }
508
509 #[test]
510 fn definition_finds_type_declaration() {
511 let src = "\
512module m
513
514public record Point { x: Int, y: Int }
515
516fn origin() -> Point {
517 Point { x: 0, y: 0 }
518}
519";
520 let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 4, 17)
522 .expect("definition found");
523 let source = result.source_map.get_file(result.file_id);
524 assert_eq!(source.slice(result.target), "Point");
525 }
526
527 #[test]
528 fn definition_finds_enum_variant_constructor() {
529 let src = "\
530module m
531
532public enum Color { Red, Green, Blue }
533
534fn favorite() -> Color {
535 Red
536}
537";
538 let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 5, 4)
540 .expect("definition found");
541 let source = result.source_map.get_file(result.file_id);
542 assert_eq!(source.slice(result.target), "Red");
543 }
544
545 #[test]
546 fn definition_returns_none_when_cursor_is_off_any_identifier() {
547 let src = "\
548module m
549
550fn main() {
551 let x = 1
552}
553";
554 assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 0).is_none());
556 }
557}