Skip to main content

bock_lsp/
goto_definition.rs

1//! `textDocument/definition` support — resolve an identifier at a cursor
2//! position to the span of its declaration.
3//!
4//! The flow is:
5//! 1. Lex + parse + resolve the current buffer (single-file mode; no
6//!    cross-file symbol resolution).
7//! 2. Translate the LSP [`Position`] to a byte offset.
8//! 3. Walk the AST to find the innermost [`Expr::Identifier`] containing
9//!    that offset, while simultaneously collecting a map of `NodeId ->
10//!    declaration Span` so we can answer the lookup.
11//! 4. Query the populated [`SymbolTable`] for the identifier's resolved
12//!    `def_id`, then look up the recorded declaration span.
13//!
14//! Cross-file go-to-definition is out of scope for F.1.3 — every
15//! declaration span returned here belongs to the buffer being checked.
16
17use 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
35/// Result of a successful go-to-definition lookup.
36pub struct DefinitionResult {
37    /// Owned source map containing the document (keeps `SourceFile`
38    /// borrows valid for the lifetime of the result).
39    pub source_map: SourceMap,
40    /// The [`FileId`] that the returned span belongs to. For single-file
41    /// checks this always equals the id of the document being queried.
42    pub file_id: FileId,
43    /// Span of the declaration being pointed at.
44    pub target: Span,
45}
46
47/// Run the minimum pipeline needed to answer a definition query and return
48/// the declaration span at the cursor, or `None` if the cursor is not over
49/// a resolved identifier.
50#[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    // Lex + parse. Even if diagnostics fire, we still try — a partial AST
64    // is often good enough to answer a definition query.
65    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    // Resolve (single-file — no cross-file registry).
71    let registry = ModuleRegistry::new();
72    let mut symbols = SymbolTable::new();
73    let _ = resolve_names_with_registry(&module, &mut symbols, &registry);
74
75    // Walk the AST: find the innermost identifier containing `offset` and
76    // build a NodeId -> declaration Span index in the same pass.
77    let mut finder = DefinitionFinder::new(offset);
78    finder.collect_toplevel_names(&module);
79    finder.visit_module(&module);
80
81    // Prefer an expression-identifier match (which can be resolved via
82    // the symbol table) over a bare type-reference name match.
83    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/// Convert a 0-indexed LSP `(line, character)` position to a byte offset
103/// into `content`. Column counts Unicode scalar values (matches the LSP
104/// UTF-16 default closely enough for Bock's ASCII-dominant source, and
105/// exactly for pure-ASCII files).
106///
107/// Returns `None` if `line` is past the end of the file.
108#[must_use]
109pub fn position_to_offset(content: &str, line: u32, character: u32) -> Option<usize> {
110    // Locate the start of the target line.
111    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    // Walk `character` characters into that line, stopping early at EOL
131    // or EOF so we clamp to the end of the line rather than overshooting.
132    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
146// ─── AST walker ──────────────────────────────────────────────────────────────
147
148/// Single-pass visitor that:
149///   - Records declaration spans keyed by NodeId (`def_spans`).
150///   - Finds the innermost `Expr::Identifier` whose span contains a target
151///     byte offset (`identifier_id`).
152struct DefinitionFinder {
153    offset: usize,
154    def_spans: HashMap<NodeId, Span>,
155    /// Map of top-level declaration name → decl span. Used as a fallback
156    /// when the cursor is on a type reference (type refs never create
157    /// entries in `SymbolTable::resolutions`; the resolver only calls
158    /// `mark_used` for them).
159    toplevel_by_name: HashMap<String, Span>,
160    identifier_id: Option<NodeId>,
161    /// Width of the innermost identifier match found so far. Smaller =
162    /// more specific; we prefer tighter matches when nesting occurs.
163    best_width: usize,
164    /// Name of a [`TypePath`] segment under the cursor, if any.
165    type_ref_name: Option<String>,
166    /// Width of the innermost type-ref match found so far.
167    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    /// Pre-pass: index every top-level named declaration by name so
192    /// type references can resolve by string lookup.
193    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    /// Check each segment of a [`TypePath`]; if the cursor is on one,
237    /// remember its name as a candidate for name-based lookup.
238    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                    // Shorthand `{ name }` bindings use a synthetic
271                    // NodeId inside the resolver, so we can't resolve
272                    // them here — skip.
273                }
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        // Walk into the initializer so identifiers inside it are still
358        // considered for the position-to-node lookup.
359        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                // Identifiers have no children; no recursion needed.
384            }
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); // 'r'
422    }
423
424    #[test]
425    fn position_to_offset_clamps_past_eol() {
426        // `character` past end of line should clamp to the newline,
427        // not jump onto the next line.
428        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        // "é" is 2 bytes (U+00E9); cursor at column 2 (0-indexed) lands
439        // after 'é', i.e. at byte offset 3.
440        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        // Cursor on the `greet` call inside caller(): line 7, column 4.
457        let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 7, 4)
458            .expect("definition found");
459        // Target should be the fn name in the declaration (line 2, "greet").
460        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        // Cursor on `answer` use (line 4, col 4).
477        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        // Cursor on `undefined_name` (line 3, col 4).
493        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        // Cursor on `print` — a builtin with no in-buffer declaration.
506        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        // Cursor on `Point` in the return-type annotation (line 4, col 17).
521        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        // Cursor on `Red` at line 5, col 4.
539        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        // Cursor in the middle of whitespace on line 3 col 0.
555        assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 0).is_none());
556    }
557}