Skip to main content

shape_lsp/
definition.rs

1//! Go-to-definition and find references provider
2//!
3//! Enables navigation to symbol definitions and finding all references.
4
5use crate::annotation_discovery::AnnotationDiscovery;
6use crate::document::DocumentManager;
7use crate::module_cache::ModuleCache;
8use crate::type_inference::infer_variable_type;
9use crate::util::{get_word_at_position, offset_to_line_col, position_to_offset};
10use shape_ast::ast::{
11    ImportItems, Item, Program, Span, Statement, TraitMember, TraitMemberSignature, TypeName,
12};
13use shape_ast::parser::parse_program;
14use std::path::{Path, PathBuf};
15use tower_lsp_server::ls_types::{
16    DocumentHighlight, DocumentHighlightKind, GotoDefinitionResponse, Location, Position, Range,
17    Uri,
18};
19
20/// Find the definition of a symbol at the given position.
21///
22/// When `cached_program` is provided, it is used as a fallback AST when
23/// the current source text fails to parse.
24pub fn get_definition(
25    text: &str,
26    position: Position,
27    uri: &Uri,
28    module_cache: Option<&ModuleCache>,
29    annotation_discovery: Option<&AnnotationDiscovery>,
30    cached_program: Option<&Program>,
31) -> Option<GotoDefinitionResponse> {
32    // Get the word at cursor
33    let word = get_word_at_position(text, position)?;
34
35    // Parse the current file, falling back to cached program or resilient parser
36    let program = match parse_program(text) {
37        Ok(p) => p,
38        Err(_) => {
39            if let Some(cached) = cached_program {
40                cached.clone()
41            } else {
42                // Fall back to resilient parser — always succeeds with partial results
43                let partial = shape_ast::parser::resilient::parse_program_resilient(text);
44                if partial.items.is_empty() {
45                    return None;
46                }
47                partial.into_program()
48            }
49        }
50    };
51
52    // First, try to find definition in the current file
53    if let Some(location) = find_definition_location(&program, &word, uri, text) {
54        return Some(GotoDefinitionResponse::Scalar(location));
55    }
56
57    // If not found locally, check if it's an imported symbol
58    if let Some(cache) = module_cache {
59        if let Some(location) = find_imported_definition(&program, &word, uri, cache) {
60            return Some(GotoDefinitionResponse::Scalar(location));
61        }
62    }
63
64    // Check if it's an annotation
65    if let Some(discovery) = annotation_discovery {
66        if let Some(location) = find_annotation_definition(&word, discovery, uri) {
67            return Some(GotoDefinitionResponse::Scalar(location));
68        }
69    }
70
71    None
72}
73
74/// Find all references to a symbol at the given position.
75///
76/// Uses scope-aware resolution via `ScopeTree` to correctly handle
77/// variable shadowing and lexical scoping.
78pub fn get_references(text: &str, position: Position, uri: &Uri) -> Option<Vec<Location>> {
79    get_references_with_fallback(text, position, uri, None)
80}
81
82/// W2.6 — Find all references to a symbol at the given position, including
83/// cross-file references from open documents and workspace `.shape` files.
84///
85/// Algorithm:
86///  1. Resolve the symbol locally via `ScopeTree::references_of` (same as
87///     `get_references_with_fallback`).
88///  2. If the local binding is module-scope-visible (top-level item — fn /
89///     type / trait / enum / pub var), enumerate other files (open docs +
90///     workspace `.shape` files via `ModuleCache::enumerate_workspace_shape_files`)
91///     and scan each one's `ScopeTree` for **module-scope** references to the
92///     same name. Lexical scoping within each file is preserved by
93///     `ScopeTree::references_of` — only top-level usages cross.
94///  3. Locally-scoped bindings (loop vars, closure params, block lets) do not
95///     produce cross-file results.
96///
97/// Returns `None` if no references are found anywhere.
98pub fn get_references_cross_file(
99    text: &str,
100    position: Position,
101    uri: &Uri,
102    cached_program: Option<&Program>,
103    documents: Option<&DocumentManager>,
104    module_cache: Option<&ModuleCache>,
105    workspace_root: Option<&Path>,
106) -> Option<Vec<Location>> {
107    // Local file references via ScopeTree (and text-search fallback).
108    let mut locations = get_references_with_fallback(text, position, uri, cached_program)
109        .unwrap_or_default();
110
111    // Determine the symbol name + whether it's module-scope-visible.
112    let Some(word) = get_word_at_position(text, position) else {
113        return if locations.is_empty() {
114            None
115        } else {
116            Some(locations)
117        };
118    };
119
120    // Parse to inspect the binding kind. If parse fails, skip cross-file.
121    let program = match parse_program(text) {
122        Ok(p) => p,
123        Err(_) => match cached_program {
124            Some(p) => p.clone(),
125            None => {
126                return if locations.is_empty() {
127                    None
128                } else {
129                    Some(locations)
130                };
131            }
132        },
133    };
134
135    if !is_module_scope_symbol(&program, &word) {
136        // Local-scope binding (loop var, closure param, block let) — no
137        // cross-file lookup. Return whatever local refs we found.
138        return if locations.is_empty() {
139            None
140        } else {
141            Some(locations)
142        };
143    }
144
145    // Cross-file scan: open documents (other than current uri) + workspace
146    // .shape files (de-duplicated).
147    let mut visited: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
148    if let Some(current_path) = uri.to_file_path() {
149        visited.insert(current_path.into_owned());
150    }
151
152    if let Some(docs) = documents {
153        for other_uri in docs.all_uris() {
154            if &other_uri == uri {
155                continue;
156            }
157            let Some(other_path_cow) = other_uri.to_file_path() else {
158                continue;
159            };
160            let other_path = other_path_cow.into_owned();
161            if !visited.insert(other_path.clone()) {
162                continue;
163            }
164            let Some(other_doc) = docs.get(&other_uri) else {
165                continue;
166            };
167            let other_text = other_doc.text();
168            collect_module_scope_refs_in_file(
169                &other_text,
170                &other_uri,
171                &word,
172                &mut locations,
173            );
174        }
175    }
176
177    if let (Some(cache), Some(root)) = (module_cache, workspace_root) {
178        let _ = cache; // Reserved for future symbol-identity refinement.
179        for path in cache.enumerate_workspace_shape_files(root) {
180            if !visited.insert(path.clone()) {
181                continue;
182            }
183            let Some(other_uri) = Uri::from_file_path(&path) else {
184                continue;
185            };
186            // Skip files already visited via open documents.
187            let Ok(other_text) = std::fs::read_to_string(&path) else {
188                continue;
189            };
190            collect_module_scope_refs_in_file(
191                &other_text,
192                &other_uri,
193                &word,
194                &mut locations,
195            );
196        }
197    }
198
199    if locations.is_empty() {
200        None
201    } else {
202        Some(locations)
203    }
204}
205
206/// Return true when `name` is bound at module (top-level) scope in
207/// `program` — i.e. it is a candidate for cross-file references because
208/// other files could import or refer to it.
209///
210/// W2.6 conservative heuristic: top-level fn / type / trait / enum /
211/// struct / foreign-fn / module-level variable declarations are
212/// module-scope-visible. Statement-nested `let`, loop vars, closure
213/// params are not.
214fn is_module_scope_symbol(program: &Program, name: &str) -> bool {
215    for item in &program.items {
216        match item {
217            Item::Function(func, _) if func.name == name => return true,
218            Item::ForeignFunction(func, _) if func.name == name => return true,
219            Item::Trait(t, _) if t.name == name => return true,
220            Item::Enum(e, _) if e.name == name => return true,
221            Item::TypeAlias(ta, _) if ta.name == name => return true,
222            Item::StructType(s, _) if s.name == name => return true,
223            Item::VariableDecl(decl, _) => {
224                for (n, _) in crate::symbols::get_pattern_names(&decl.pattern) {
225                    if n == name {
226                        return true;
227                    }
228                }
229            }
230            Item::Statement(Statement::VariableDecl(decl, _), _) => {
231                for (n, _) in crate::symbols::get_pattern_names(&decl.pattern) {
232                    if n == name {
233                        return true;
234                    }
235                }
236            }
237            Item::Import(import_stmt, _) => match &import_stmt.items {
238                ImportItems::Named(specs) => {
239                    for spec in specs {
240                        let local = spec.alias.as_ref().unwrap_or(&spec.name);
241                        if local == name {
242                            return true;
243                        }
244                    }
245                }
246                ImportItems::Namespace { name: ns_name, alias } => {
247                    let local = alias.as_ref().unwrap_or(ns_name);
248                    if local == name {
249                        return true;
250                    }
251                }
252            },
253            _ => {}
254        }
255    }
256    false
257}
258
259/// Collect ScopeTree references to a top-level `name` in `text`, appending
260/// `Location`s into `out`. Only the module-scope binding (and its
261/// references) is considered — locally-shadowing inner bindings are
262/// excluded by `ScopeTree::references_of` semantics.
263fn collect_module_scope_refs_in_file(
264    text: &str,
265    uri: &Uri,
266    name: &str,
267    out: &mut Vec<Location>,
268) {
269    let program = match parse_program(text) {
270        Ok(p) => p,
271        Err(_) => {
272            let partial = shape_ast::parse_program_resilient(text);
273            if partial.items.is_empty() {
274                return;
275            }
276            partial.into_program()
277        }
278    };
279
280    if !is_module_scope_symbol(&program, name) {
281        return;
282    }
283
284    let tree = crate::scope::ScopeTree::build(&program, text);
285    // Find any module-scope binding with this name. ScopeTree's first
286    // scope is the module (root) scope; its bindings are the top-level
287    // names.
288    let Some(root) = tree.scopes.first() else {
289        return;
290    };
291    for binding in &root.bindings {
292        if binding.name != name {
293            continue;
294        }
295        let push = |span: (usize, usize), out: &mut Vec<Location>| {
296            let (sl, sc) = offset_to_line_col(text, span.0);
297            let (el, ec) = offset_to_line_col(text, span.1);
298            out.push(Location {
299                uri: uri.clone(),
300                range: Range {
301                    start: Position {
302                        line: sl,
303                        character: sc,
304                    },
305                    end: Position {
306                        line: el,
307                        character: ec,
308                    },
309                },
310            });
311        };
312        push(binding.def_span, out);
313        for span in &binding.references {
314            push(*span, out);
315        }
316    }
317}
318
319/// Find all references to a symbol at the given position, with cached program fallback.
320pub fn get_references_with_fallback(
321    text: &str,
322    position: Position,
323    uri: &Uri,
324    cached_program: Option<&Program>,
325) -> Option<Vec<Location>> {
326    // Get the byte offset of the cursor
327    let offset = position_to_offset(text, position)?;
328
329    // Parse, falling back to cached program or resilient parser
330    let program = match parse_program(text) {
331        Ok(p) => p,
332        Err(_) => {
333            if let Some(cached) = cached_program {
334                cached.clone()
335            } else {
336                let partial = shape_ast::parse_program_resilient(text);
337                if partial.items.is_empty() {
338                    return None;
339                }
340                partial.into_program()
341            }
342        }
343    };
344    let tree = crate::scope::ScopeTree::build(&program, text);
345
346    // Find all references (def + uses) via scope-aware resolution
347    let spans = tree.references_of(offset)?;
348
349    let locations: Vec<Location> = spans
350        .into_iter()
351        .map(|(start, end)| {
352            let (start_line, start_col) = offset_to_line_col(text, start);
353            let (end_line, end_col) = offset_to_line_col(text, end);
354            Location {
355                uri: uri.clone(),
356                range: Range {
357                    start: Position {
358                        line: start_line,
359                        character: start_col,
360                    },
361                    end: Position {
362                        line: end_line,
363                        character: end_col,
364                    },
365                },
366            }
367        })
368        .collect();
369
370    if locations.is_empty() {
371        // Fallback to text-based search if scope tree didn't find anything
372        let word = get_word_at_position(text, position)?;
373        let fallback = find_all_references(&program, &word, uri, text);
374        if fallback.is_empty() {
375            None
376        } else {
377            Some(fallback)
378        }
379    } else {
380        Some(locations)
381    }
382}
383
384/// Find the definition site of the *type* of the symbol at the cursor.
385///
386/// W2.5 feature 1.33 (`textDocument/typeDefinition`): given an expression
387/// (typically a variable identifier), look up its inferred type name and
388/// then navigate to that type's declaration site.
389///
390/// Strategy:
391/// 1. Extract the word at the cursor.
392/// 2. Use `infer_variable_type` to determine the variable's type as a string.
393/// 3. Strip generic wrappers (e.g. `Array<T>` → `T`, `Option<T>` → `T`,
394///    `HashMap<K,V>` → `K` falls back to `V`) and dereference / postfix `?`
395///    decorations to recover a base type name.
396/// 4. Re-use `find_definition_location` to look up that type name in the
397///    current file, then fall back to imported definitions.
398///
399/// Returns `None` when no type can be inferred or when the inferred type is
400/// a primitive (`int`, `number`, `bool`, `string`, etc.) for which no
401/// user-visible definition site exists.
402pub fn get_type_definition(
403    text: &str,
404    position: Position,
405    uri: &Uri,
406    module_cache: Option<&ModuleCache>,
407    cached_program: Option<&Program>,
408) -> Option<GotoDefinitionResponse> {
409    let word = get_word_at_position(text, position)?;
410
411    let program = match parse_program(text) {
412        Ok(p) => p,
413        Err(_) => {
414            if let Some(cached) = cached_program {
415                cached.clone()
416            } else {
417                let partial = shape_ast::parser::resilient::parse_program_resilient(text);
418                if partial.items.is_empty() {
419                    return None;
420                }
421                partial.into_program()
422            }
423        }
424    };
425
426    let inferred = infer_variable_type(&program, &word)?;
427    let base = extract_base_type_name(&inferred)?;
428    if is_builtin_primitive(&base) {
429        return None;
430    }
431
432    if let Some(location) = find_definition_location(&program, &base, uri, text) {
433        return Some(GotoDefinitionResponse::Scalar(location));
434    }
435
436    if let Some(cache) = module_cache {
437        if let Some(location) = find_imported_definition(&program, &base, uri, cache) {
438            return Some(GotoDefinitionResponse::Scalar(location));
439        }
440    }
441
442    None
443}
444
445/// Find all `impl Trait for Type` blocks for the symbol at the cursor.
446///
447/// W2.5 feature 1.34 (`textDocument/implementation`): when the cursor is on
448/// a trait name, return every impl block in the current file that targets
449/// that trait. When the cursor is on a type name, return every impl block
450/// (and extend block) whose target type matches.
451///
452/// LSP-K (audit §D regression flow): when the cursor is on a trait-method
453/// name (inside the trait body, either `Required` signature or `Default`
454/// method), return every matching impl-method body location across all
455/// impl blocks in the current file that target the enclosing trait. This
456/// is the trait-method → impl-method jump the audit §A.1 surfaced as
457/// returning null.
458///
459/// This is a current-file-only enumeration; cross-workspace impl discovery
460/// is part of the broader workspace-symbol indexing work tracked in W2.6/W2.7.
461pub fn get_implementations(
462    text: &str,
463    position: Position,
464    uri: &Uri,
465    cached_program: Option<&Program>,
466) -> Option<Vec<Location>> {
467    let word = get_word_at_position(text, position)?;
468
469    let program = match parse_program(text) {
470        Ok(p) => p,
471        Err(_) => {
472            if let Some(cached) = cached_program {
473                cached.clone()
474            } else {
475                let partial = shape_ast::parser::resilient::parse_program_resilient(text);
476                if partial.items.is_empty() {
477                    return None;
478                }
479                partial.into_program()
480            }
481        }
482    };
483
484    // LSP-K: trait-method → impl-method path. If the cursor sits on a
485    // trait-method name (`Required` signature or `Default` method) inside
486    // a trait body, surface every impl-method body that overrides it.
487    if let Some(cursor_offset) = position_to_offset(text, position) {
488        if let Some((trait_name, method_name)) =
489            find_trait_method_at_offset(&program, cursor_offset, &word)
490        {
491            let impl_method_locations =
492                collect_impl_method_locations(&program, trait_name, method_name, uri, text);
493            if !impl_method_locations.is_empty() {
494                return Some(impl_method_locations);
495            }
496        }
497    }
498
499    let mut locations: Vec<Location> = Vec::new();
500
501    for item in &program.items {
502        match item {
503            Item::Impl(impl_block, item_span) => {
504                let trait_str = type_name_str(&impl_block.trait_name);
505                let target_str = type_name_str(&impl_block.target_type);
506                if trait_str == word || target_str == word {
507                    locations.push(create_location_from_span(uri, *item_span, text));
508                }
509            }
510            Item::Extend(extend_stmt, item_span) => {
511                let target_str = type_name_str(&extend_stmt.type_name);
512                if target_str == word {
513                    locations.push(create_location_from_span(uri, *item_span, text));
514                }
515            }
516            _ => {}
517        }
518    }
519
520    if locations.is_empty() {
521        None
522    } else {
523        Some(locations)
524    }
525}
526
527/// LSP-K helper: detect whether the cursor sits on a trait-method name
528/// (in a `Required` signature or `Default` method) inside the body of a
529/// `trait` item in the parsed program.
530///
531/// Returns `Some((trait_name, method_name))` when the offset falls inside
532/// a trait-member span AND the cursor word matches the method's name.
533/// The word match prevents drilling into impl-methods when the cursor is
534/// on a method body keyword/identifier that happens to share a name with
535/// another trait-method.
536fn find_trait_method_at_offset<'p>(
537    program: &'p Program,
538    offset: usize,
539    word: &str,
540) -> Option<(&'p str, &'p str)> {
541    for item in &program.items {
542        if let Item::Trait(trait_def, _trait_span) = item {
543            for member in &trait_def.members {
544                match member {
545                    TraitMember::Required(TraitMemberSignature::Method { name, span, .. }) => {
546                        if offset >= span.start && offset <= span.end && name == word {
547                            return Some((trait_def.name.as_str(), name.as_str()));
548                        }
549                    }
550                    TraitMember::Default(method) => {
551                        let span = method.span;
552                        if offset >= span.start && offset <= span.end && method.name == word {
553                            return Some((trait_def.name.as_str(), method.name.as_str()));
554                        }
555                    }
556                    _ => {}
557                }
558            }
559        }
560    }
561    None
562}
563
564/// LSP-K helper: collect impl-method body `Location`s for every
565/// `impl <trait_name> for X` block whose method list contains a method
566/// named `method_name`.
567fn collect_impl_method_locations(
568    program: &Program,
569    trait_name: &str,
570    method_name: &str,
571    uri: &Uri,
572    text: &str,
573) -> Vec<Location> {
574    let mut locations: Vec<Location> = Vec::new();
575    for item in &program.items {
576        if let Item::Impl(impl_block, _) = item {
577            if type_name_str(&impl_block.trait_name) != trait_name {
578                continue;
579            }
580            for method in &impl_block.methods {
581                if method.name == method_name {
582                    locations.push(create_location_from_span(uri, method.span, text));
583                }
584            }
585        }
586    }
587    locations
588}
589
590/// Find the declaration site of a symbol at the cursor.
591///
592/// W2.5 feature 1.35 (`textDocument/declaration`): in languages with a
593/// declaration/definition split (C++, etc.) this jumps to the declaration
594/// (e.g. header file). Shape has no such split — every binding is its own
595/// declaration — so this aliases to `get_definition` for VS-Code-style
596/// `Ctrl+Click → Go to Declaration` interop.
597pub fn get_declaration(
598    text: &str,
599    position: Position,
600    uri: &Uri,
601    module_cache: Option<&ModuleCache>,
602    annotation_discovery: Option<&AnnotationDiscovery>,
603    cached_program: Option<&Program>,
604) -> Option<GotoDefinitionResponse> {
605    get_definition(
606        text,
607        position,
608        uri,
609        module_cache,
610        annotation_discovery,
611        cached_program,
612    )
613}
614
615/// Find all occurrences of the symbol at the cursor within the current file.
616///
617/// W2.5 feature 1.60 (`textDocument/documentHighlight`): editors use these
618/// to highlight every usage of the cursor's symbol in the active file
619/// (e.g. background-tint every `myVar` when the cursor is on one of them).
620///
621/// Reuses the scope-aware `ScopeTree::references_of` infrastructure that
622/// powers in-file rename (`rename.rs`) so highlights respect lexical scope
623/// and variable shadowing — falling back to text search only when the scope
624/// tree cannot bind the position.
625pub fn get_document_highlights(
626    text: &str,
627    position: Position,
628    cached_program: Option<&Program>,
629) -> Option<Vec<DocumentHighlight>> {
630    let offset = position_to_offset(text, position)?;
631
632    let program = match parse_program(text) {
633        Ok(p) => p,
634        Err(_) => {
635            if let Some(cached) = cached_program {
636                cached.clone()
637            } else {
638                let partial = shape_ast::parse_program_resilient(text);
639                if partial.items.is_empty() {
640                    return None;
641                }
642                partial.into_program()
643            }
644        }
645    };
646
647    let tree = crate::scope::ScopeTree::build(&program, text);
648
649    let spans = tree.references_of(offset);
650
651    let highlights: Vec<DocumentHighlight> = match spans {
652        Some(spans) => spans
653            .into_iter()
654            .map(|(start, end)| span_to_highlight(text, start, end))
655            .collect(),
656        None => {
657            // Fall back to text-based search to match the find-references behaviour.
658            let word = get_word_at_position(text, position)?;
659            text_search_highlights(text, &word)
660        }
661    };
662
663    if highlights.is_empty() {
664        None
665    } else {
666        Some(highlights)
667    }
668}
669
670/// Word-boundary text-search variant of `find_all_references`, returning
671/// `DocumentHighlight` ranges (no URI required — highlights are always
672/// scoped to the active file per LSP spec).
673fn text_search_highlights(text: &str, symbol_name: &str) -> Vec<DocumentHighlight> {
674    let mut highlights = Vec::new();
675    let lines: Vec<&str> = text.lines().collect();
676    for (line_idx, line) in lines.iter().enumerate() {
677        let mut char_pos = 0;
678        while let Some(pos) = line[char_pos..].find(symbol_name) {
679            let absolute_pos = char_pos + pos;
680            let is_start_boundary = absolute_pos == 0
681                || !line
682                    .chars()
683                    .nth(absolute_pos - 1)
684                    .map(|c| c.is_alphanumeric() || c == '_')
685                    .unwrap_or(false);
686            let is_end_boundary = absolute_pos + symbol_name.len() >= line.len()
687                || !line
688                    .chars()
689                    .nth(absolute_pos + symbol_name.len())
690                    .map(|c| c.is_alphanumeric() || c == '_')
691                    .unwrap_or(false);
692
693            if is_start_boundary && is_end_boundary {
694                highlights.push(DocumentHighlight {
695                    range: Range {
696                        start: Position {
697                            line: line_idx as u32,
698                            character: absolute_pos as u32,
699                        },
700                        end: Position {
701                            line: line_idx as u32,
702                            character: (absolute_pos + symbol_name.len()) as u32,
703                        },
704                    },
705                    kind: Some(DocumentHighlightKind::TEXT),
706                });
707            }
708            char_pos = absolute_pos + symbol_name.len();
709        }
710    }
711    highlights
712}
713
714fn span_to_highlight(text: &str, start: usize, end: usize) -> DocumentHighlight {
715    let (start_line, start_col) = offset_to_line_col(text, start);
716    let (end_line, end_col) = offset_to_line_col(text, end);
717    DocumentHighlight {
718        range: Range {
719            start: Position {
720                line: start_line,
721                character: start_col,
722            },
723            end: Position {
724                line: end_line,
725                character: end_col,
726            },
727        },
728        kind: Some(DocumentHighlightKind::TEXT),
729    }
730}
731
732/// Render a `TypeName` AST node as its bare leading identifier.
733fn type_name_str(type_name: &TypeName) -> &str {
734    match type_name {
735        TypeName::Simple(n) => n.as_str(),
736        TypeName::Generic { name, .. } => name.as_str(),
737    }
738}
739
740/// Extract the user-visible base identifier from a rendered type string.
741///
742/// Inputs are produced by `type_inference::infer_variable_type`, which renders
743/// strings like `"Array<Point>"`, `"Option<Point>"`, `"Point"`, `"Point?"`,
744/// `"&Point"`. This function peels generic wrappers / decorations and returns
745/// the inner-most identifier suitable for a `find_definition_location` lookup.
746///
747/// Returns `None` for empty / whitespace-only / object-shape strings (`"{...}"`)
748/// where no nameable type exists.
749fn extract_base_type_name(rendered: &str) -> Option<String> {
750    let mut current: String = rendered.trim().to_string();
751    if current.is_empty() || current.starts_with('{') {
752        return None;
753    }
754
755    // Strip leading reference markers.
756    loop {
757        let trimmed = current.trim_start();
758        if let Some(rest) = trimmed.strip_prefix("&mut ") {
759            current = rest.trim_start().to_string();
760        } else if let Some(rest) = trimmed.strip_prefix('&') {
761            current = rest.trim_start().to_string();
762        } else {
763            break;
764        }
765    }
766
767    // Strip trailing `?` (Option sugar) until none remain.
768    while let Some(rest) = current.strip_suffix('?') {
769        current = rest.trim_end().to_string();
770    }
771
772    // Unwrap one level of common generic carriers — repeated to handle
773    // `Array<Option<Point>>`-style nesting.
774    loop {
775        let unwrapped: Option<String> = if let Some(inner) = strip_generic_wrapper(&current, "Array")
776        {
777            Some(inner.to_string())
778        } else if let Some(inner) = strip_generic_wrapper(&current, "Option") {
779            Some(inner.to_string())
780        } else if let Some(inner) = strip_generic_wrapper(&current, "Result") {
781            // Result<T, E> — first generic arg is the success type.
782            Some(first_generic_arg(inner).to_string())
783        } else if let Some(inner) = strip_generic_wrapper(&current, "HashMap") {
784            // HashMap<K, V> — first arg keeps it deterministic.
785            Some(first_generic_arg(inner).to_string())
786        } else {
787            None
788        };
789        match unwrapped {
790            Some(inner) => {
791                let inner_trim = inner.trim().to_string();
792                if inner_trim == current {
793                    break;
794                }
795                current = inner_trim;
796            }
797            None => break,
798        }
799    }
800
801    // Final shape: must be a single identifier (no `<`, no whitespace, no `,`).
802    let base = current
803        .split(|c: char| c == '<' || c == ',' || c.is_whitespace())
804        .next()?;
805    let base = base.trim();
806    if base.is_empty() {
807        None
808    } else {
809        Some(base.to_string())
810    }
811}
812
813/// If `s` matches `Name<...>`, return the `...` portion. Else `None`.
814fn strip_generic_wrapper<'a>(s: &'a str, name: &str) -> Option<&'a str> {
815    let s = s.strip_prefix(name)?.trim_start();
816    let s = s.strip_prefix('<')?;
817    let s = s.strip_suffix('>')?;
818    Some(s)
819}
820
821fn first_generic_arg(args: &str) -> &str {
822    args.split(',').next().unwrap_or(args).trim()
823}
824
825/// Primitive type names that have no user-visible definition site.
826fn is_builtin_primitive(name: &str) -> bool {
827    matches!(
828        name,
829        "int"
830            | "number"
831            | "bool"
832            | "string"
833            | "decimal"
834            | "bigint"
835            | "unit"
836            | "null"
837            | "DateTime"
838            | "unknown"
839            | "any"
840    )
841}
842
843/// Find the location where a symbol is defined
844fn find_definition_location(
845    program: &Program,
846    symbol_name: &str,
847    uri: &Uri,
848    text: &str,
849) -> Option<Location> {
850    for item in &program.items {
851        match item {
852            Item::Function(func, _) if func.name == symbol_name => {
853                return Some(create_location_from_span(uri, func.name_span, text));
854            }
855            Item::VariableDecl(var_decl, item_span) => {
856                for (name, name_span) in crate::symbols::get_pattern_names(&var_decl.pattern) {
857                    if name == symbol_name {
858                        let span = if name_span.is_dummy() {
859                            *item_span
860                        } else {
861                            name_span
862                        };
863                        return Some(create_location_from_span(uri, span, text));
864                    }
865                }
866            }
867            Item::Statement(Statement::VariableDecl(var_decl, stmt_span), _) => {
868                for (name, name_span) in crate::symbols::get_pattern_names(&var_decl.pattern) {
869                    if name == symbol_name {
870                        let span = if name_span.is_dummy() {
871                            *stmt_span
872                        } else {
873                            name_span
874                        };
875                        return Some(create_location_from_span(uri, span, text));
876                    }
877                }
878            }
879            Item::TypeAlias(type_alias, item_span) if type_alias.name == symbol_name => {
880                return Some(create_location_from_span(uri, *item_span, text));
881            }
882            Item::Enum(enum_def, item_span) if enum_def.name == symbol_name => {
883                return Some(create_location_from_span(uri, *item_span, text));
884            }
885            Item::Trait(trait_def, item_span) if trait_def.name == symbol_name => {
886                return Some(create_location_from_span(uri, *item_span, text));
887            }
888            Item::Impl(impl_block, _) => {
889                // Navigate from impl method name to trait definition
890                let trait_name_str = match &impl_block.trait_name {
891                    shape_ast::ast::TypeName::Simple(n) => n.as_str(),
892                    shape_ast::ast::TypeName::Generic { name, .. } => name.as_str(),
893                };
894                // If clicking on the trait name in `impl TraitName for Type`,
895                // navigate to the trait definition
896                if trait_name_str == symbol_name {
897                    // Find the trait definition elsewhere in the program
898                    for other_item in &program.items {
899                        if let Item::Trait(td, ts) = other_item {
900                            if td.name == symbol_name {
901                                return Some(create_location_from_span(uri, *ts, text));
902                            }
903                        }
904                    }
905                }
906                // If clicking on a method name inside the impl block,
907                // navigate to the trait's method signature
908                for method in &impl_block.methods {
909                    if method.name == symbol_name {
910                        // Find the trait definition and navigate to the method member
911                        for other_item in &program.items {
912                            if let Item::Trait(td, ts) = other_item {
913                                if td.name == trait_name_str {
914                                    // Return trait span (method-level spans not yet available)
915                                    return Some(create_location_from_span(uri, *ts, text));
916                                }
917                            }
918                        }
919                    }
920                }
921            }
922            Item::Extend(extend_stmt, item_span) => {
923                // Navigate from method name usage to method definition in extend block
924                for method in &extend_stmt.methods {
925                    if method.name == symbol_name {
926                        return Some(create_location_from_span(uri, *item_span, text));
927                    }
928                }
929            }
930            Item::StructType(struct_def, item_span) if struct_def.name == symbol_name => {
931                return Some(create_location_from_span(uri, *item_span, text));
932            }
933            _ => {}
934        }
935    }
936
937    // Sprint 4L: If the symbol is "format" and it's a method call on a typed object,
938    // try to navigate to the Display trait impl for that type.
939    if symbol_name == "format" || symbol_name == "toString" {
940        // Find impl Display for ... blocks
941        for item in &program.items {
942            if let Item::Impl(impl_block, item_span) = item {
943                let trait_name_str = match &impl_block.trait_name {
944                    shape_ast::ast::TypeName::Simple(n) => n.as_str(),
945                    shape_ast::ast::TypeName::Generic { name, .. } => name.as_str(),
946                };
947                if trait_name_str == "Display" {
948                    // Check if the impl block has a method matching our symbol
949                    for method in &impl_block.methods {
950                        if method.name == symbol_name {
951                            return Some(create_location_from_span(uri, *item_span, text));
952                        }
953                    }
954                    // Even if method not found, navigate to impl block
955                    return Some(create_location_from_span(uri, *item_span, text));
956                }
957            }
958        }
959    }
960
961    None
962}
963
964/// Find all references to a symbol in the program
965fn find_all_references(
966    _program: &Program,
967    symbol_name: &str,
968    uri: &Uri,
969    text: &str,
970) -> Vec<Location> {
971    let mut locations = Vec::new();
972    let lines: Vec<&str> = text.lines().collect();
973
974    // Simple approach: find all occurrences of the symbol name in the text
975    // A more sophisticated approach would parse the AST and find identifier references
976    for (line_idx, line) in lines.iter().enumerate() {
977        let mut char_pos = 0;
978        while let Some(pos) = line[char_pos..].find(symbol_name) {
979            let absolute_pos = char_pos + pos;
980
981            // Check if it's a word boundary (not part of another identifier)
982            let is_start_boundary = absolute_pos == 0
983                || !line
984                    .chars()
985                    .nth(absolute_pos - 1)
986                    .map(|c| c.is_alphanumeric() || c == '_')
987                    .unwrap_or(false);
988
989            let is_end_boundary = absolute_pos + symbol_name.len() >= line.len()
990                || !line
991                    .chars()
992                    .nth(absolute_pos + symbol_name.len())
993                    .map(|c| c.is_alphanumeric() || c == '_')
994                    .unwrap_or(false);
995
996            if is_start_boundary && is_end_boundary {
997                locations.push(Location {
998                    uri: uri.clone(),
999                    range: Range {
1000                        start: Position {
1001                            line: line_idx as u32,
1002                            character: absolute_pos as u32,
1003                        },
1004                        end: Position {
1005                            line: line_idx as u32,
1006                            character: (absolute_pos + symbol_name.len()) as u32,
1007                        },
1008                    },
1009                });
1010            }
1011
1012            char_pos = absolute_pos + symbol_name.len();
1013        }
1014    }
1015
1016    locations
1017}
1018
1019/// Create a location from a span
1020fn create_location_from_span(uri: &Uri, span: Span, text: &str) -> Location {
1021    let (start_line, start_col) = offset_to_line_col(text, span.start);
1022    let (end_line, end_col) = offset_to_line_col(text, span.end);
1023
1024    Location {
1025        uri: uri.clone(),
1026        range: Range {
1027            start: Position {
1028                line: start_line,
1029                character: start_col,
1030            },
1031            end: Position {
1032                line: end_line,
1033                character: end_col,
1034            },
1035        },
1036    }
1037}
1038
1039/// Find the definition of a symbol in imported modules
1040fn find_imported_definition(
1041    program: &Program,
1042    symbol_name: &str,
1043    current_uri: &Uri,
1044    module_cache: &ModuleCache,
1045) -> Option<Location> {
1046    // Get current file path from URI
1047    let current_path = current_uri.to_file_path()?.into_owned();
1048
1049    // Look through import statements to find where the symbol comes from
1050    for item in &program.items {
1051        if let Item::Import(import_stmt, _span) = item {
1052            // Check if this import includes the symbol we're looking for
1053            let imports_symbol = match &import_stmt.items {
1054                ImportItems::Named(specs) => specs.iter().any(|spec| {
1055                    let imported_name = spec.alias.as_ref().unwrap_or(&spec.name);
1056                    imported_name == symbol_name
1057                }),
1058                ImportItems::Namespace { name, alias } => {
1059                    let local_name = alias.as_ref().unwrap_or(name);
1060                    local_name == symbol_name
1061                }
1062            };
1063
1064            if !imports_symbol {
1065                continue;
1066            }
1067
1068            // Resolve the import path
1069            // Note: module resolution uses dot-separated paths.
1070            // std. imports resolve via stdlib path.
1071            let resolved_path =
1072                module_cache.resolve_import(&import_stmt.from, &current_path, None)?;
1073
1074            // Load the module
1075            let module_info =
1076                module_cache.load_module_with_context(&resolved_path, &current_path, None)?;
1077
1078            // Find the symbol in the module's exports
1079            for export in &module_info.exports {
1080                if export.exported_name() == symbol_name {
1081                    // Find the actual definition in the module's program
1082                    let target_uri = Uri::from_file_path(&module_info.path)?;
1083                    let source = std::fs::read_to_string(&module_info.path).ok()?;
1084
1085                    // Look up the definition in the target module
1086                    let location = find_definition_location(
1087                        &module_info.program,
1088                        &export.name,
1089                        &target_uri,
1090                        &source,
1091                    )?;
1092
1093                    return Some(location);
1094                }
1095            }
1096        }
1097    }
1098
1099    None
1100}
1101
1102/// Find the definition of an annotation
1103fn find_annotation_definition(
1104    annotation_name: &str,
1105    annotation_discovery: &AnnotationDiscovery,
1106    current_uri: &Uri,
1107) -> Option<Location> {
1108    // Get annotation info
1109    let info = annotation_discovery.get(annotation_name)?;
1110
1111    // If the annotation has a valid location (not a built-in), create a location
1112    if info.location != Span::default() {
1113        // Determine which file the annotation is defined in
1114        let target_uri = if let Some(ref source_path) = info.source_file {
1115            // Imported annotation — navigate to the source file
1116            Uri::from_file_path(source_path)?
1117        } else {
1118            // Local annotation — same file
1119            current_uri.clone()
1120        };
1121
1122        // Read source to convert byte offset to line/col
1123        let source = if let Some(ref source_path) = info.source_file {
1124            std::fs::read_to_string(source_path).ok()?
1125        } else {
1126            // For local annotations, we'd need the current file text
1127            // but we don't have it here. Return a reasonable position.
1128            return Some(Location {
1129                uri: target_uri,
1130                range: Range {
1131                    start: Position {
1132                        line: 0,
1133                        character: 0,
1134                    },
1135                    end: Position {
1136                        line: 0,
1137                        character: 0,
1138                    },
1139                },
1140            });
1141        };
1142
1143        let (line, col) = offset_to_line_col(&source, info.location.start);
1144        Some(Location {
1145            uri: target_uri,
1146            range: Range {
1147                start: Position {
1148                    line,
1149                    character: col,
1150                },
1151                end: Position {
1152                    line,
1153                    character: col + annotation_name.len() as u32,
1154                },
1155            },
1156        })
1157    } else {
1158        None
1159    }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    #[test]
1167    fn test_get_word_at_position() {
1168        let text = "let myVar = 5;";
1169        let word = get_word_at_position(
1170            text,
1171            Position {
1172                line: 0,
1173                character: 5,
1174            },
1175        );
1176        assert_eq!(word, Some("myVar".to_string()));
1177    }
1178
1179    #[test]
1180    fn test_find_function_definition() {
1181        let code = r#"function myFunc(x, y) {
1182    return x + y;
1183}
1184
1185let result = myFunc(1, 2);
1186"#;
1187        let program = parse_program(code).unwrap();
1188        let uri = Uri::from_file_path("/test.shape").unwrap();
1189
1190        let location = find_definition_location(&program, "myFunc", &uri, code);
1191        assert!(location.is_some());
1192
1193        let loc = location.unwrap();
1194        assert_eq!(loc.range.start.line, 0);
1195        // Should point to the function name "myFunc"
1196        assert_eq!(loc.range.start.character, 9); // "function " is 9 chars
1197    }
1198
1199    #[test]
1200    fn test_find_variable_definition() {
1201        let code = r#"let myVar = 42;
1202let x = myVar + 5;
1203"#;
1204        let program = parse_program(code).unwrap();
1205        let uri = Uri::from_file_path("/test.shape").unwrap();
1206
1207        let location = find_definition_location(&program, "myVar", &uri, code);
1208        assert!(location.is_some());
1209    }
1210
1211    #[test]
1212    fn test_find_references() {
1213        let code = r#"let myVar = 42;
1214let x = myVar + 5;
1215let y = myVar * 2;
1216"#;
1217        let program = parse_program(code).unwrap();
1218        let uri = Uri::from_file_path("/test.shape").unwrap();
1219
1220        let refs = find_all_references(&program, "myVar", &uri, code);
1221        assert_eq!(refs.len(), 3); // Definition + 2 usages
1222    }
1223
1224    #[test]
1225    fn test_get_definition_with_module_cache() {
1226        let code = r#"function localFunc() {
1227    return 42;
1228}
1229"#;
1230        let uri = Uri::from_file_path("/test.shape").unwrap();
1231        let cache = ModuleCache::new();
1232
1233        let definition = get_definition(
1234            code,
1235            Position {
1236                line: 0,
1237                character: 10,
1238            },
1239            &uri,
1240            Some(&cache),
1241            None,
1242            None,
1243        );
1244        assert!(definition.is_some());
1245    }
1246
1247    #[test]
1248    fn test_find_imported_definition_not_found() {
1249        let code = r#"from utils use { foo };
1250
1251let x = foo();
1252"#;
1253        let program = parse_program(code).unwrap();
1254        let uri = Uri::from_file_path("/test.shape").unwrap();
1255        let cache = ModuleCache::new();
1256
1257        // This will return None because the module doesn't exist
1258        let location = find_imported_definition(&program, "foo", &uri, &cache);
1259        assert!(location.is_none());
1260    }
1261
1262    // --- W2.5: definition family extras (typeDefinition / implementation /
1263    //          declaration / documentHighlight) tests ---
1264
1265    #[test]
1266    fn test_extract_base_type_name_plain() {
1267        assert_eq!(extract_base_type_name("Point"), Some("Point".to_string()));
1268    }
1269
1270    #[test]
1271    fn test_extract_base_type_name_array_wrapper() {
1272        assert_eq!(
1273            extract_base_type_name("Array<Point>"),
1274            Some("Point".to_string())
1275        );
1276    }
1277
1278    #[test]
1279    fn test_extract_base_type_name_option_question_mark() {
1280        assert_eq!(extract_base_type_name("Point?"), Some("Point".to_string()));
1281    }
1282
1283    #[test]
1284    fn test_extract_base_type_name_option_wrapper() {
1285        assert_eq!(
1286            extract_base_type_name("Option<Point>"),
1287            Some("Point".to_string())
1288        );
1289    }
1290
1291    #[test]
1292    fn test_extract_base_type_name_reference() {
1293        assert_eq!(
1294            extract_base_type_name("&mut Point"),
1295            Some("Point".to_string())
1296        );
1297        assert_eq!(extract_base_type_name("&Point"), Some("Point".to_string()));
1298    }
1299
1300    #[test]
1301    fn test_extract_base_type_name_nested() {
1302        assert_eq!(
1303            extract_base_type_name("Array<Option<Point>>"),
1304            Some("Point".to_string())
1305        );
1306    }
1307
1308    #[test]
1309    fn test_extract_base_type_name_result() {
1310        assert_eq!(
1311            extract_base_type_name("Result<Point, Error>"),
1312            Some("Point".to_string())
1313        );
1314    }
1315
1316    #[test]
1317    fn test_extract_base_type_name_object_shape_skipped() {
1318        // Object-literal shape strings have no nameable type to navigate to.
1319        assert_eq!(extract_base_type_name("{ x: int, y: int }"), None);
1320    }
1321
1322    #[test]
1323    fn test_is_builtin_primitive_filters_int() {
1324        assert!(is_builtin_primitive("int"));
1325        assert!(is_builtin_primitive("string"));
1326        assert!(!is_builtin_primitive("Point"));
1327    }
1328
1329    #[test]
1330    fn test_get_implementations_finds_impl_block() {
1331        let code = r#"trait Greet {
1332    method greet() -> string
1333}
1334
1335type Cat { name: string }
1336
1337impl Greet for Cat {
1338    method greet() { return "meow" }
1339}
1340"#;
1341        // Sanity-check parse — surface any imprecision in test fixture syntax.
1342        let program = parse_program(code).expect("test fixture must parse");
1343        let impl_count = program
1344            .items
1345            .iter()
1346            .filter(|i| matches!(i, Item::Impl(_, _)))
1347            .count();
1348        assert!(impl_count >= 1, "Expected at least 1 Item::Impl in parsed program");
1349
1350        let uri = Uri::from_file_path("/test.shape").unwrap();
1351        // Cursor on "Greet" trait name in the impl line (line 6, after "impl ").
1352        let impls = get_implementations(
1353            code,
1354            Position {
1355                line: 6,
1356                character: 6,
1357            },
1358            &uri,
1359            None,
1360        );
1361        assert!(impls.is_some(), "Should find impl block for trait Greet");
1362        let locations = impls.unwrap();
1363        assert_eq!(locations.len(), 1);
1364    }
1365
1366    #[test]
1367    fn test_get_implementations_by_target_type() {
1368        // Cursor on the *target* type (Cat) should also surface the impl.
1369        let code = r#"trait Greet {
1370    method greet() -> string
1371}
1372
1373type Cat { name: string }
1374
1375impl Greet for Cat {
1376    method greet() { return "meow" }
1377}
1378"#;
1379        let program = parse_program(code).expect("test fixture must parse");
1380        assert!(program.items.iter().any(|i| matches!(i, Item::Impl(_, _))));
1381
1382        let uri = Uri::from_file_path("/test.shape").unwrap();
1383        // Cursor on "Cat" target — line 6, char 16 ("impl Greet for Cat" — 'C' of Cat).
1384        let impls = get_implementations(
1385            code,
1386            Position {
1387                line: 6,
1388                character: 16,
1389            },
1390            &uri,
1391            None,
1392        );
1393        assert!(
1394            impls.is_some(),
1395            "Should find impl block when cursor is on target type Cat"
1396        );
1397    }
1398
1399    #[test]
1400    fn test_get_declaration_aliases_definition() {
1401        let code = r#"let myVar = 42;
1402let x = myVar + 5;
1403"#;
1404        let uri = Uri::from_file_path("/test.shape").unwrap();
1405        // Cursor on `myVar` use on line 1.
1406        let decl = get_declaration(
1407            code,
1408            Position {
1409                line: 1,
1410                character: 9,
1411            },
1412            &uri,
1413            None,
1414            None,
1415            None,
1416        );
1417        let def = get_definition(
1418            code,
1419            Position {
1420                line: 1,
1421                character: 9,
1422            },
1423            &uri,
1424            None,
1425            None,
1426            None,
1427        );
1428        assert_eq!(decl.is_some(), def.is_some());
1429    }
1430
1431    #[test]
1432    fn test_get_document_highlights_finds_variable_uses() {
1433        let code = r#"let myVar = 42;
1434let x = myVar + 5;
1435let y = myVar * 2;
1436"#;
1437        // Cursor on the definition site of `myVar`.
1438        let highlights = get_document_highlights(
1439            code,
1440            Position {
1441                line: 0,
1442                character: 6,
1443            },
1444            None,
1445        );
1446        assert!(highlights.is_some(), "Should find highlights for myVar");
1447        let hs = highlights.unwrap();
1448        assert!(
1449            hs.len() >= 3,
1450            "Expected at least 3 highlights (def + 2 uses), got {}",
1451            hs.len()
1452        );
1453        for h in &hs {
1454            assert_eq!(h.kind, Some(DocumentHighlightKind::TEXT));
1455        }
1456    }
1457
1458    #[test]
1459    fn test_get_document_highlights_returns_none_off_symbol() {
1460        let code = "let myVar = 42;\n";
1461        // Cursor on whitespace before `let`.
1462        let highlights = get_document_highlights(
1463            code,
1464            Position {
1465                line: 0,
1466                character: 0,
1467            },
1468            None,
1469        );
1470        // ScopeTree won't bind at offset 0, fallback path needs a word — none here.
1471        // Result depends on the surrounding text; ensure the call doesn't panic.
1472        let _ = highlights;
1473    }
1474
1475    #[test]
1476    fn test_is_module_scope_symbol_top_level_fn() {
1477        let code = "fn foo() { return 1 }\nlet x = foo()";
1478        let program = parse_program(code).unwrap();
1479        assert!(is_module_scope_symbol(&program, "foo"));
1480        assert!(is_module_scope_symbol(&program, "x"));
1481        // Inner locals are not module-scope-visible.
1482        assert!(!is_module_scope_symbol(&program, "nope"));
1483    }
1484
1485    #[test]
1486    fn test_collect_module_scope_refs_finds_call_site() {
1487        let text = "fn helper() { return 1 }\nlet x = helper() + helper()";
1488        let uri = Uri::from_file_path("/other.shape").unwrap();
1489        let mut out = Vec::new();
1490        collect_module_scope_refs_in_file(text, &uri, "helper", &mut out);
1491        // def + 2 references = 3 locations
1492        assert!(
1493            out.len() >= 3,
1494            "expected def + at least 2 refs to `helper`, got {}",
1495            out.len()
1496        );
1497    }
1498
1499    #[test]
1500    fn test_get_references_cross_file_module_scope() {
1501        // Simulate two open documents that both reference `shared`.
1502        use crate::document::DocumentManager;
1503        let docs = DocumentManager::new();
1504        let main_text = "fn shared() { return 1 }\nlet a = shared()".to_string();
1505        let other_text = "fn shared() { return 2 }\nlet b = shared() + shared()".to_string();
1506        let main_uri = Uri::from_file_path("/main.shape").unwrap();
1507        let other_uri = Uri::from_file_path("/other.shape").unwrap();
1508        docs.open(main_uri.clone(), 1, main_text.clone());
1509        docs.open(other_uri.clone(), 1, other_text);
1510
1511        // Click on `shared` in main.shape (col 3 of `fn shared()`)
1512        let pos = Position {
1513            line: 0,
1514            character: 3,
1515        };
1516        let refs = get_references_cross_file(
1517            &main_text,
1518            pos,
1519            &main_uri,
1520            None,
1521            Some(&docs),
1522            None,
1523            None,
1524        )
1525        .expect("should find cross-file references");
1526        // Local def + local 1 use + other def + other 2 uses = 5
1527        assert!(
1528            refs.len() >= 4,
1529            "expected cross-file refs, got {}: {:?}",
1530            refs.len(),
1531            refs
1532        );
1533        assert!(
1534            refs.iter().any(|loc| &loc.uri == &other_uri),
1535            "expected at least one reference from /other.shape"
1536        );
1537    }
1538
1539    #[test]
1540    fn test_get_references_cross_file_local_binding_no_crossover() {
1541        // Local `let` inside a function — should NOT cascade to other files.
1542        use crate::document::DocumentManager;
1543        let docs = DocumentManager::new();
1544        let main_text =
1545            "fn outer() {\n  let local = 1\n  return local + local\n}".to_string();
1546        let other_text =
1547            "fn other() {\n  let local = 5\n  return local\n}".to_string();
1548        let main_uri = Uri::from_file_path("/main.shape").unwrap();
1549        let other_uri = Uri::from_file_path("/other.shape").unwrap();
1550        docs.open(main_uri.clone(), 1, main_text.clone());
1551        docs.open(other_uri.clone(), 1, other_text);
1552
1553        // Click on `local` in main.shape at its definition site
1554        let local_offset = main_text.find("local").unwrap();
1555        let (line, col) = offset_to_line_col(&main_text, local_offset);
1556        let pos = Position {
1557            line,
1558            character: col,
1559        };
1560        let refs = get_references_cross_file(
1561            &main_text,
1562            pos,
1563            &main_uri,
1564            None,
1565            Some(&docs),
1566            None,
1567            None,
1568        );
1569        // Expect refs only in main.shape (local-scope binding)
1570        if let Some(refs) = refs {
1571            assert!(
1572                refs.iter().all(|loc| &loc.uri == &main_uri),
1573                "local-scope `local` should NOT cascade to other files, got: {:?}",
1574                refs
1575            );
1576        }
1577    }
1578
1579    #[test]
1580    fn test_references_with_broken_code() {
1581        // Code with a syntax error after a valid function
1582        let code =
1583            "fn greet(name) {\n  return name\n}\nlet x = greet(\"hi\")\n??broken syntax here";
1584        let uri = Uri::from_file_path("/test.shape").unwrap();
1585
1586        // Position on "greet" in the function definition (line 0, char 3)
1587        let refs = get_references_with_fallback(
1588            code,
1589            Position {
1590                line: 0,
1591                character: 3,
1592            },
1593            &uri,
1594            None, // no cached program, relies on resilient parser
1595        );
1596
1597        // With resilient parsing, we should find at least the definition of "greet"
1598        assert!(
1599            refs.is_some(),
1600            "Should find references even with broken code via resilient parsing"
1601        );
1602    }
1603
1604    // W14.2-B1: per-line coverage additions
1605    #[test]
1606    fn test_get_definition_returns_none_for_non_identifier_position() {
1607        let code = "let x = 5\n";
1608        let uri = Uri::from_file_path("/test.shape").unwrap();
1609        // Position on `=` (non-identifier)
1610        let result = get_definition(
1611            code,
1612            Position {
1613                line: 0,
1614                character: 6,
1615            },
1616            &uri,
1617            None,
1618            None,
1619            None,
1620        );
1621        assert!(result.is_none(), "expected None at non-identifier position");
1622    }
1623
1624    #[test]
1625    fn test_get_definition_finds_function_at_call_site() {
1626        let code = "fn myFunc() { return 1 }\nlet x = myFunc()\n";
1627        let uri = Uri::from_file_path("/test.shape").unwrap();
1628        // Position on `myFunc` at call site
1629        let result = get_definition(
1630            code,
1631            Position {
1632                line: 1,
1633                character: 9,
1634            },
1635            &uri,
1636            None,
1637            None,
1638            None,
1639        );
1640        assert!(result.is_some(), "expected definition at call site");
1641    }
1642
1643    #[test]
1644    fn test_get_definition_unknown_symbol_returns_none() {
1645        let code = "let x = neverDefined\n";
1646        let uri = Uri::from_file_path("/test.shape").unwrap();
1647        // Position on `neverDefined`
1648        let result = get_definition(
1649            code,
1650            Position {
1651                line: 0,
1652                character: 8,
1653            },
1654            &uri,
1655            None,
1656            None,
1657            None,
1658        );
1659        assert!(result.is_none(), "expected None for unknown symbol");
1660    }
1661
1662    #[test]
1663    fn test_get_type_definition_returns_none_for_unknown_symbol() {
1664        let code = "let x = 5\n";
1665        let uri = Uri::from_file_path("/test.shape").unwrap();
1666        let result = get_type_definition(
1667            code,
1668            Position {
1669                line: 0,
1670                character: 4,
1671            },
1672            &uri,
1673            None,
1674            None,
1675        );
1676        // int is a primitive — type definition returns None.
1677        assert!(result.is_none(), "expected None for primitive type");
1678    }
1679
1680    #[test]
1681    fn test_get_implementations_returns_none_for_no_impls() {
1682        let code = "let x = 5\n";
1683        let uri = Uri::from_file_path("/test.shape").unwrap();
1684        let result = get_implementations(
1685            code,
1686            Position {
1687                line: 0,
1688                character: 4,
1689            },
1690            &uri,
1691            None,
1692        );
1693        assert!(result.is_none(), "expected None when no impls exist");
1694    }
1695
1696    #[test]
1697    fn test_get_implementations_finds_impl_by_trait_name() {
1698        let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1699        let uri = Uri::from_file_path("/test.shape").unwrap();
1700        // Position on "Q" in the trait header at line 2 (0-indexed)
1701        let result = get_implementations(
1702            code,
1703            Position {
1704                line: 2,
1705                character: 5,
1706            },
1707            &uri,
1708            None,
1709        );
1710        assert!(result.is_some(), "expected impl-block location for Q");
1711        let locs = result.unwrap();
1712        assert!(!locs.is_empty());
1713    }
1714
1715    // == LSP-K: trait-method → impl-method body jump ============================
1716
1717    #[test]
1718    fn test_get_implementations_jumps_from_trait_method_to_impl_method() {
1719        // LSP-K: cursor on the trait-method name `q` (line 0 char 13, the 'q'
1720        // inside `fn q(self)` of the trait body) must surface the impl-method
1721        // body, not the impl block as a whole.
1722        let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1723        let uri = Uri::from_file_path("/test.shape").unwrap();
1724        let result = get_implementations(
1725            code,
1726            Position {
1727                line: 0,
1728                character: 13,
1729            },
1730            &uri,
1731            None,
1732        );
1733        assert!(
1734            result.is_some(),
1735            "expected impl-method location for trait-method 'q'"
1736        );
1737        let locs = result.unwrap();
1738        assert_eq!(locs.len(), 1, "expected exactly one impl-method match");
1739        // The returned range must fall inside the impl block on line 2 — that
1740        // is, NOT the trait declaration itself on line 0.
1741        assert_eq!(locs[0].range.start.line, 2);
1742    }
1743
1744    #[test]
1745    fn test_get_implementations_trait_method_multi_impls() {
1746        // Multiple impls of the same trait — all impl-method bodies must
1747        // appear in the result.
1748        let code = "trait Greet {\n    fn hello(self) -> string;\n}\ntype Cat { name: string }\ntype Dog { name: string }\nimpl Greet for Cat {\n    fn hello(self) -> string { return \"meow\" }\n}\nimpl Greet for Dog {\n    fn hello(self) -> string { return \"woof\" }\n}\n";
1749        let uri = Uri::from_file_path("/test.shape").unwrap();
1750        // Cursor on the trait-method name `hello` (line 1).
1751        let result = get_implementations(
1752            code,
1753            Position {
1754                line: 1,
1755                character: 7,
1756            },
1757            &uri,
1758            None,
1759        );
1760        assert!(
1761            result.is_some(),
1762            "expected impl-method locations for trait-method 'hello'"
1763        );
1764        let locs = result.unwrap();
1765        assert_eq!(locs.len(), 2, "expected two impl-method matches");
1766    }
1767
1768    #[test]
1769    fn test_get_implementations_default_trait_method_still_finds_impls() {
1770        // Cursor on a default-method name in a trait body — impls that
1771        // override it must still surface.
1772        let code = "trait Greet {\n    fn hello(self) -> string { return \"hi\" }\n}\ntype Cat { name: string }\nimpl Greet for Cat {\n    fn hello(self) -> string { return \"meow\" }\n}\n";
1773        let uri = Uri::from_file_path("/test.shape").unwrap();
1774        // Cursor on `hello` in the default-method body's signature line.
1775        let result = get_implementations(
1776            code,
1777            Position {
1778                line: 1,
1779                character: 7,
1780            },
1781            &uri,
1782            None,
1783        );
1784        assert!(
1785            result.is_some(),
1786            "expected impl-method location for default trait-method 'hello'"
1787        );
1788        let locs = result.unwrap();
1789        // Default-method body is itself in the trait — the impl override is
1790        // the only "implementation" target.
1791        assert_eq!(locs.len(), 1);
1792    }
1793
1794    #[test]
1795    fn test_get_implementations_trait_name_still_returns_impl_block() {
1796        // Regression guard for the existing trait-name → impl-block path
1797        // (LSP-D's existing behavior must not be broken by LSP-K).
1798        let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1799        let uri = Uri::from_file_path("/test.shape").unwrap();
1800        // Cursor on the trait name `Q` in the impl header.
1801        let result = get_implementations(
1802            code,
1803            Position {
1804                line: 2,
1805                character: 5,
1806            },
1807            &uri,
1808            None,
1809        );
1810        assert!(result.is_some(), "trait-name path must still resolve");
1811        let locs = result.unwrap();
1812        assert_eq!(locs.len(), 1);
1813        // The trait-name path returns the impl BLOCK range, which begins on
1814        // line 2 (start of `impl Q for T { ... }`).
1815        assert_eq!(locs[0].range.start.line, 2);
1816    }
1817
1818    #[test]
1819    fn test_get_implementations_trait_method_no_matching_impl() {
1820        // Trait-method exists, but no impl provides it — return None
1821        // (the impl-method path must not fall back to the impl-block path
1822        // here, since the cursor is on a trait-method, not a trait-name).
1823        let code = "trait Q { fn q(self) -> int; }\n";
1824        let uri = Uri::from_file_path("/test.shape").unwrap();
1825        let result = get_implementations(
1826            code,
1827            Position {
1828                line: 0,
1829                character: 13,
1830            },
1831            &uri,
1832            None,
1833        );
1834        // No impls at all → None is correct (current-file enumeration).
1835        assert!(result.is_none());
1836    }
1837
1838    #[test]
1839    fn test_get_document_highlights_returns_none_off_word() {
1840        let code = "let x = 5\n";
1841        let result = get_document_highlights(
1842            code,
1843            Position {
1844                line: 0,
1845                character: 6, // on `=`
1846            },
1847            None,
1848        );
1849        // Off any identifier — should still be Some or None gracefully.
1850        // The function should not panic; success is just-don't-crash.
1851        let _ = result;
1852    }
1853
1854    #[test]
1855    fn test_get_references_cross_file_no_documents_or_cache() {
1856        let code = "fn foo() { return 1 }\nlet x = foo()\n";
1857        let uri = Uri::from_file_path("/test.shape").unwrap();
1858        let result = get_references_cross_file(
1859            code,
1860            Position {
1861                line: 0,
1862                character: 3,
1863            },
1864            &uri,
1865            None, // cached_program
1866            None, // documents
1867            None, // module_cache
1868            None, // workspace_root
1869        );
1870        // Should still find local references via get_references.
1871        assert!(result.is_some(), "expected local references when no cross-file context");
1872    }
1873
1874    #[test]
1875    fn test_get_declaration_aliases_get_definition() {
1876        let code = "fn foo() { return 1 }\nlet x = foo()\n";
1877        let uri = Uri::from_file_path("/test.shape").unwrap();
1878        let pos = Position {
1879            line: 1,
1880            character: 9,
1881        };
1882        let decl = get_declaration(code, pos, &uri, None, None, None);
1883        let def = get_definition(code, pos, &uri, None, None, None);
1884        // Should produce equivalent results (both Some or both None)
1885        assert_eq!(decl.is_some(), def.is_some());
1886    }
1887}