Skip to main content

solidity_language_server/
goto.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Location, Position, Range, Url};
4use tree_sitter::{Node, Parser};
5
6use crate::types::{NodeId, SourceLoc};
7
8#[derive(Debug, Clone)]
9pub struct NodeInfo {
10    pub src: String,
11    pub name_location: Option<String>,
12    pub name_locations: Vec<String>,
13    pub referenced_declaration: Option<NodeId>,
14    pub node_type: Option<String>,
15    pub member_location: Option<String>,
16    pub absolute_path: Option<String>,
17}
18
19/// All AST child keys to traverse (Solidity + Yul).
20pub const CHILD_KEYS: &[&str] = &[
21    "AST",
22    "arguments",
23    "baseContracts",
24    "baseExpression",
25    "baseName",
26    "baseType",
27    "block",
28    "body",
29    "components",
30    "condition",
31    "declarations",
32    "endExpression",
33    "errorCall",
34    "eventCall",
35    "expression",
36    "externalCall",
37    "falseBody",
38    "falseExpression",
39    "file",
40    "foreign",
41    "functionName",
42    "indexExpression",
43    "initialValue",
44    "initializationExpression",
45    "keyType",
46    "leftExpression",
47    "leftHandSide",
48    "libraryName",
49    "literals",
50    "loopExpression",
51    "members",
52    "modifierName",
53    "modifiers",
54    "name",
55    "names",
56    "nodes",
57    "options",
58    "overrides",
59    "parameters",
60    "pathNode",
61    "post",
62    "pre",
63    "returnParameters",
64    "rightExpression",
65    "rightHandSide",
66    "startExpression",
67    "statements",
68    "storageLayout",
69    "subExpression",
70    "subdenomination",
71    "symbolAliases",
72    "trueBody",
73    "trueExpression",
74    "typeName",
75    "unitAlias",
76    "value",
77    "valueType",
78    "variableNames",
79    "variables",
80];
81
82fn push_if_node_or_array<'a>(tree: &'a Value, key: &str, stack: &mut Vec<&'a Value>) {
83    if let Some(value) = tree.get(key) {
84        match value {
85            Value::Array(arr) => {
86                stack.extend(arr);
87            }
88            Value::Object(_) => {
89                stack.push(value);
90            }
91            _ => {}
92        }
93    }
94}
95
96/// Maps `"offset:length:fileId"` src strings from Yul externalReferences
97/// to the Solidity declaration node id they refer to.
98pub type ExternalRefs = HashMap<String, NodeId>;
99
100/// Pre-computed AST index. Built once when an AST enters the cache,
101/// then reused on every goto/references/rename/hover request.
102#[derive(Debug, Clone)]
103pub struct CachedBuild {
104    pub ast: Value,
105    pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
106    pub path_to_abs: HashMap<String, String>,
107    pub external_refs: ExternalRefs,
108    pub id_to_path_map: HashMap<String, String>,
109    /// Pre-built gas index from contract output. Built once, reused by
110    /// hover, inlay hints, and code lens.
111    pub gas_index: crate::gas::GasIndex,
112    /// Pre-built hint lookup per file. Built once, reused on every
113    /// inlay hint request (avoids O(n²) declaration resolution per request).
114    pub hint_index: crate::inlay_hints::HintIndex,
115    /// Pre-built documentation index from solc userdoc/devdoc.
116    /// Merged and keyed by selector for fast hover lookup.
117    pub doc_index: crate::hover::DocIndex,
118    /// The text_cache version this build was created from.
119    /// Used to detect dirty files (unsaved edits since last build).
120    pub build_version: i32,
121}
122
123impl CachedBuild {
124    /// Build the index from normalized AST output.
125    ///
126    /// Canonical shape:
127    /// - `sources[path] = { id, ast }`
128    /// - `contracts[path][name] = { abi, evm, ... }`
129    /// - `source_id_to_path = { "0": "path", ... }`
130    pub fn new(ast: Value, build_version: i32) -> Self {
131        let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
132            cache_ids(sources)
133        } else {
134            (HashMap::new(), HashMap::new(), HashMap::new())
135        };
136
137        let id_to_path_map = ast
138            .get("source_id_to_path")
139            .and_then(|v| v.as_object())
140            .map(|obj| {
141                obj.iter()
142                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
143                    .collect()
144            })
145            .unwrap_or_default();
146
147        let gas_index = crate::gas::build_gas_index(&ast);
148
149        let hint_index = if let Some(sources) = ast.get("sources") {
150            crate::inlay_hints::build_hint_index(sources)
151        } else {
152            HashMap::new()
153        };
154
155        let doc_index = crate::hover::build_doc_index(&ast);
156
157        Self {
158            ast,
159            nodes,
160            path_to_abs,
161            external_refs,
162            id_to_path_map,
163            gas_index,
164            hint_index,
165            doc_index,
166            build_version,
167        }
168    }
169}
170
171type Type = (
172    HashMap<String, HashMap<NodeId, NodeInfo>>,
173    HashMap<String, String>,
174    ExternalRefs,
175);
176
177pub fn cache_ids(sources: &Value) -> Type {
178    let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> = HashMap::new();
179    let mut path_to_abs: HashMap<String, String> = HashMap::new();
180    let mut external_refs: ExternalRefs = HashMap::new();
181
182    if let Some(sources_obj) = sources.as_object() {
183        for (path, source_data) in sources_obj {
184            if let Some(ast) = source_data.get("ast") {
185                // Get the absolute path for this file
186                let abs_path = ast
187                    .get("absolutePath")
188                    .and_then(|v| v.as_str())
189                    .unwrap_or(path)
190                    .to_string();
191
192                path_to_abs.insert(path.clone(), abs_path.clone());
193
194                // Initialize the nodes map for this file
195                if !nodes.contains_key(&abs_path) {
196                    nodes.insert(abs_path.clone(), HashMap::new());
197                }
198
199                if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
200                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
201                {
202                    nodes.get_mut(&abs_path).unwrap().insert(
203                        NodeId(id),
204                        NodeInfo {
205                            src: src.to_string(),
206                            name_location: None,
207                            name_locations: vec![],
208                            referenced_declaration: None,
209                            node_type: ast
210                                .get("nodeType")
211                                .and_then(|v| v.as_str())
212                                .map(|s| s.to_string()),
213                            member_location: None,
214                            absolute_path: ast
215                                .get("absolutePath")
216                                .and_then(|v| v.as_str())
217                                .map(|s| s.to_string()),
218                        },
219                    );
220                }
221
222                let mut stack = vec![ast];
223
224                while let Some(tree) = stack.pop() {
225                    if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
226                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
227                    {
228                        let id = NodeId(raw_id);
229                        // Check for nameLocation first
230                        let mut name_location = tree
231                            .get("nameLocation")
232                            .and_then(|v| v.as_str())
233                            .map(|s| s.to_string());
234
235                        // Check for nameLocations array and use appropriate element
236                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
237                        // For other nodes, use the first element
238                        if name_location.is_none()
239                            && let Some(name_locations) = tree.get("nameLocations")
240                            && let Some(locations_array) = name_locations.as_array()
241                            && !locations_array.is_empty()
242                        {
243                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
244                            if node_type == Some("IdentifierPath") {
245                                name_location = locations_array
246                                    .last()
247                                    .and_then(|v| v.as_str())
248                                    .map(|s| s.to_string());
249                            } else {
250                                name_location = locations_array[0].as_str().map(|s| s.to_string());
251                            }
252                        }
253
254                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
255                            && let Some(locations_array) = name_locations.as_array()
256                        {
257                            locations_array
258                                .iter()
259                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
260                                .collect()
261                        } else {
262                            vec![]
263                        };
264
265                        let mut final_name_location = name_location;
266                        if final_name_location.is_none()
267                            && let Some(member_loc) =
268                                tree.get("memberLocation").and_then(|v| v.as_str())
269                        {
270                            final_name_location = Some(member_loc.to_string());
271                        }
272
273                        let node_info = NodeInfo {
274                            src: src.to_string(),
275                            name_location: final_name_location,
276                            name_locations,
277                            referenced_declaration: tree
278                                .get("referencedDeclaration")
279                                .and_then(|v| v.as_u64())
280                                .map(NodeId),
281                            node_type: tree
282                                .get("nodeType")
283                                .and_then(|v| v.as_str())
284                                .map(|s| s.to_string()),
285                            member_location: tree
286                                .get("memberLocation")
287                                .and_then(|v| v.as_str())
288                                .map(|s| s.to_string()),
289                            absolute_path: tree
290                                .get("absolutePath")
291                                .and_then(|v| v.as_str())
292                                .map(|s| s.to_string()),
293                        };
294
295                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
296
297                        // Collect externalReferences from InlineAssembly nodes
298                        if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
299                            && let Some(ext_refs) =
300                                tree.get("externalReferences").and_then(|v| v.as_array())
301                        {
302                            for ext_ref in ext_refs {
303                                if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
304                                    && let Some(decl_id) =
305                                        ext_ref.get("declaration").and_then(|v| v.as_u64())
306                                {
307                                    external_refs.insert(src_str.to_string(), NodeId(decl_id));
308                                }
309                            }
310                        }
311                    }
312
313                    for key in CHILD_KEYS {
314                        push_if_node_or_array(tree, key, &mut stack);
315                    }
316                }
317            }
318        }
319    }
320
321    (nodes, path_to_abs, external_refs)
322}
323
324pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
325    let text = String::from_utf8_lossy(source_bytes);
326    crate::utils::position_to_byte_offset(&text, position)
327}
328
329pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
330    let text = String::from_utf8_lossy(source_bytes);
331    let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
332    Some(pos)
333}
334
335/// Convert a `"offset:length:fileId"` src string to an LSP Location.
336pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
337    let loc = SourceLoc::parse(src)?;
338    let file_path = id_to_path.get(&loc.file_id_str())?;
339
340    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
341        std::path::PathBuf::from(file_path)
342    } else {
343        std::env::current_dir().ok()?.join(file_path)
344    };
345
346    let source_bytes = std::fs::read(&absolute_path).ok()?;
347    let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
348    let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
349    let uri = Url::from_file_path(&absolute_path).ok()?;
350
351    Some(Location {
352        uri,
353        range: Range {
354            start: start_pos,
355            end: end_pos,
356        },
357    })
358}
359
360pub fn goto_bytes(
361    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
362    path_to_abs: &HashMap<String, String>,
363    id_to_path: &HashMap<String, String>,
364    external_refs: &ExternalRefs,
365    uri: &str,
366    position: usize,
367) -> Option<(String, usize, usize)> {
368    let path = match uri.starts_with("file://") {
369        true => &uri[7..],
370        false => uri,
371    };
372
373    // Get absolute path for this file
374    let abs_path = path_to_abs.get(path)?;
375
376    // Get nodes for the current file only
377    let current_file_nodes = nodes.get(abs_path)?;
378
379    // Build reverse map: file_path -> file_id for filtering external refs by current file
380    let path_to_file_id: HashMap<&str, &str> = id_to_path
381        .iter()
382        .map(|(id, p)| (p.as_str(), id.as_str()))
383        .collect();
384
385    // Determine the file id for the current file
386    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
387    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
388    let current_file_id = path_to_file_id.get(abs_path.as_str());
389
390    // Check if cursor is on a Yul external reference first
391    for (src_str, decl_id) in external_refs {
392        let Some(src_loc) = SourceLoc::parse(src_str) else {
393            continue;
394        };
395
396        // Only consider external refs in the current file
397        if let Some(file_id) = current_file_id {
398            if src_loc.file_id_str() != *file_id {
399                continue;
400            }
401        } else {
402            continue;
403        }
404
405        if src_loc.offset <= position && position < src_loc.end() {
406            // Found a Yul external reference — resolve to the declaration target
407            let mut target_node: Option<&NodeInfo> = None;
408            for file_nodes in nodes.values() {
409                if let Some(node) = file_nodes.get(decl_id) {
410                    target_node = Some(node);
411                    break;
412                }
413            }
414            let node = target_node?;
415            let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
416            let loc = SourceLoc::parse(loc_str)?;
417            let file_path = id_to_path.get(&loc.file_id_str())?.clone();
418            return Some((file_path, loc.offset, loc.length));
419        }
420    }
421
422    let mut refs = HashMap::new();
423
424    // Only consider nodes from the current file that have references
425    for (id, content) in current_file_nodes {
426        if content.referenced_declaration.is_none() {
427            continue;
428        }
429
430        let Some(src_loc) = SourceLoc::parse(&content.src) else {
431            continue;
432        };
433
434        if src_loc.offset <= position && position < src_loc.end() {
435            let diff = src_loc.length;
436            if !refs.contains_key(&diff) || refs[&diff] <= *id {
437                refs.insert(diff, *id);
438            }
439        }
440    }
441
442    if refs.is_empty() {
443        // Check if we're on the string part of an import statement
444        // ImportDirective nodes have absolutePath pointing to the imported file
445        let tmp = current_file_nodes.iter();
446        for (_id, content) in tmp {
447            if content.node_type == Some("ImportDirective".to_string()) {
448                let Some(src_loc) = SourceLoc::parse(&content.src) else {
449                    continue;
450                };
451
452                if src_loc.offset <= position
453                    && position < src_loc.end()
454                    && let Some(import_path) = &content.absolute_path
455                {
456                    return Some((import_path.clone(), 0, 0));
457                }
458            }
459        }
460        return None;
461    }
462
463    // Find the reference with minimum diff (most specific)
464    let min_diff = *refs.keys().min()?;
465    let chosen_id = refs[&min_diff];
466    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
467
468    // Search for the referenced declaration across all files
469    let mut target_node: Option<&NodeInfo> = None;
470    for file_nodes in nodes.values() {
471        if let Some(node) = file_nodes.get(&ref_id) {
472            target_node = Some(node);
473            break;
474        }
475    }
476
477    let node = target_node?;
478
479    // Get location from nameLocation or src
480    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
481    let loc = SourceLoc::parse(loc_str)?;
482    let file_path = id_to_path.get(&loc.file_id_str())?.clone();
483
484    Some((file_path, loc.offset, loc.length))
485}
486
487pub fn goto_declaration(
488    ast_data: &Value,
489    file_uri: &Url,
490    position: Position,
491    source_bytes: &[u8],
492) -> Option<Location> {
493    let sources = ast_data.get("sources")?;
494    let id_to_path = ast_data.get("source_id_to_path")?.as_object()?;
495
496    let id_to_path_map: HashMap<String, String> = id_to_path
497        .iter()
498        .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
499        .collect();
500
501    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
502    let byte_position = pos_to_bytes(source_bytes, position);
503
504    if let Some((file_path, location_bytes, length)) = goto_bytes(
505        &nodes,
506        &path_to_abs,
507        &id_to_path_map,
508        &external_refs,
509        file_uri.as_ref(),
510        byte_position,
511    ) {
512        let target_file_path = std::path::Path::new(&file_path);
513        let absolute_path = if target_file_path.is_absolute() {
514            target_file_path.to_path_buf()
515        } else {
516            std::env::current_dir().ok()?.join(target_file_path)
517        };
518
519        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
520            && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
521            && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
522            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
523        {
524            return Some(Location {
525                uri: target_uri,
526                range: Range {
527                    start: start_pos,
528                    end: end_pos,
529                },
530            });
531        }
532    };
533
534    None
535}
536
537/// Name-based AST goto — resolves by searching cached AST nodes for identifiers
538/// matching `name` in the current file, then following `referencedDeclaration`.
539///
540/// Unlike `goto_declaration` which matches by byte offset (breaks on dirty files),
541/// this reads the identifier text from the **built source** (on disk) at each node's
542/// `src` range and compares it to the cursor name. Works on dirty files because the
543/// AST node relationships (referencedDeclaration) are still valid — only the byte
544/// offsets in the current buffer are stale.
545/// `byte_hint` is the cursor's byte offset in the dirty buffer, used to pick
546/// the closest matching node when multiple nodes share the same name (overloads).
547pub fn goto_declaration_by_name(
548    cached_build: &CachedBuild,
549    file_uri: &Url,
550    name: &str,
551    byte_hint: usize,
552) -> Option<Location> {
553    let path = match file_uri.as_ref().starts_with("file://") {
554        true => &file_uri.as_ref()[7..],
555        false => file_uri.as_ref(),
556    };
557    let abs_path = cached_build.path_to_abs.get(path)?;
558    // Read the built source from disk to extract identifier text at src ranges
559    let built_source = std::fs::read_to_string(abs_path).ok()?;
560
561    // Collect all matching nodes: (distance_to_hint, span_size, ref_id)
562    let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
563
564    let mut tmp = {
565        let this = cached_build.nodes.get(abs_path)?;
566        this.iter()
567    };
568    while let Some((_id, node)) = tmp.next() {
569        let ref_id = match node.referenced_declaration {
570            Some(id) => id,
571            None => continue,
572        };
573
574        // Parse the node's src to get the byte range in the built source
575        let Some(src_loc) = SourceLoc::parse(&node.src) else {
576            continue;
577        };
578        let start = src_loc.offset;
579        let length = src_loc.length;
580
581        if start + length > built_source.len() {
582            continue;
583        }
584
585        let node_text = &built_source[start..start + length];
586
587        // Check if this node's text matches the name we're looking for.
588        // For simple identifiers, the text equals the name directly.
589        // For member access (e.g. `x.toInt128()`), check if the text contains
590        // `.name(` or ends with `.name`.
591        let matches = node_text == name
592            || node_text.contains(&format!(".{name}("))
593            || node_text.ends_with(&format!(".{name}"));
594
595        if matches {
596            // Distance from the byte_hint (cursor in dirty buffer) to the
597            // node's src range. The closest node is most likely the one the
598            // cursor is on, even if byte offsets shifted slightly.
599            let distance = if byte_hint >= start && byte_hint < start + length {
600                0 // cursor is inside this node's range
601            } else if byte_hint < start {
602                start - byte_hint
603            } else {
604                byte_hint - (start + length)
605            };
606            candidates.push((distance, length, ref_id));
607        }
608    }
609
610    // Sort by distance (closest to cursor hint), then by span size (narrowest)
611    candidates.sort_by_key(|&(dist, span, _)| (dist, span));
612    let ref_id = candidates.first()?.2;
613
614    // Find the declaration node across all files
615    let mut target_node: Option<&NodeInfo> = None;
616    for file_nodes in cached_build.nodes.values() {
617        if let Some(node) = file_nodes.get(&ref_id) {
618            target_node = Some(node);
619            break;
620        }
621    }
622
623    let node = target_node?;
624
625    // Parse the target's nameLocation or src
626    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
627    let loc = SourceLoc::parse(loc_str)?;
628
629    let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
630    let location_bytes = loc.offset;
631    let length = loc.length;
632
633    let target_file_path = std::path::Path::new(file_path);
634    let absolute_path = if target_file_path.is_absolute() {
635        target_file_path.to_path_buf()
636    } else {
637        std::env::current_dir().ok()?.join(target_file_path)
638    };
639
640    let target_source_bytes = std::fs::read(&absolute_path).ok()?;
641    let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
642    let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
643    let target_uri = Url::from_file_path(&absolute_path).ok()?;
644
645    Some(Location {
646        uri: target_uri,
647        range: Range {
648            start: start_pos,
649            end: end_pos,
650        },
651    })
652}
653
654// ── Tree-sitter enhanced goto ──────────────────────────────────────────────
655
656/// Context extracted from the cursor position via tree-sitter.
657#[derive(Debug, Clone)]
658pub struct CursorContext {
659    /// The identifier text under the cursor.
660    pub name: String,
661    /// Enclosing function name (if any).
662    pub function: Option<String>,
663    /// Enclosing contract/interface/library name (if any).
664    pub contract: Option<String>,
665    /// Object in a member access expression (e.g. `SqrtPriceMath` in
666    /// `SqrtPriceMath.getAmount0Delta`). Set when the cursor is on the
667    /// property side of a dot expression.
668    pub object: Option<String>,
669    /// Number of arguments at the call site (for overload disambiguation).
670    /// Set when the cursor is on a function name inside a `call_expression`.
671    pub arg_count: Option<usize>,
672    /// Inferred argument types at the call site (e.g. `["uint160", "uint160", "int128"]`).
673    /// `None` entries mean the type couldn't be inferred for that argument.
674    pub arg_types: Vec<Option<String>>,
675}
676
677/// Parse Solidity source with tree-sitter.
678fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
679    let mut parser = Parser::new();
680    parser
681        .set_language(&tree_sitter_solidity::LANGUAGE.into())
682        .expect("failed to load Solidity grammar");
683    parser.parse(source, None)
684}
685
686/// Validate that the text at a goto target location matches the expected name.
687///
688/// Used to reject tree-sitter results that land on the wrong identifier.
689/// AST results are NOT validated because the AST can legitimately resolve
690/// to a different name (e.g. `.selector` → error declaration).
691pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
692    let line = location.range.start.line as usize;
693    let start_col = location.range.start.character as usize;
694    let end_col = location.range.end.character as usize;
695
696    if let Some(line_text) = target_source.lines().nth(line)
697        && end_col <= line_text.len()
698    {
699        return &line_text[start_col..end_col] == expected_name;
700    }
701    // Can't read target — assume valid
702    true
703}
704
705/// Find the deepest named node at the given byte offset.
706fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
707    if byte < node.start_byte() || byte >= node.end_byte() {
708        return None;
709    }
710    let mut cursor = node.walk();
711    for child in node.children(&mut cursor) {
712        if child.start_byte() <= byte
713            && byte < child.end_byte()
714            && let Some(deeper) = ts_node_at_byte(child, byte)
715        {
716            return Some(deeper);
717        }
718    }
719    Some(node)
720}
721
722/// Get the identifier name from a node (first `identifier` child or the node itself).
723fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
724    let mut cursor = node.walk();
725    node.children(&mut cursor)
726        .find(|c| c.kind() == "identifier" && c.is_named())
727        .map(|c| &source[c.byte_range()])
728}
729
730/// Infer the type of an expression node using tree-sitter.
731///
732/// For identifiers, walks up to find the variable declaration and extracts its type.
733/// For literals, infers the type from the literal kind.
734/// For function calls, returns None (would need return type resolution).
735fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
736    // Unwrap call_argument → get inner expression
737    let expr = if arg_node.kind() == "call_argument" {
738        let mut c = arg_node.walk();
739        arg_node.children(&mut c).find(|ch| ch.is_named())?
740    } else {
741        arg_node
742    };
743
744    match expr.kind() {
745        "identifier" => {
746            let var_name = &source[expr.byte_range()];
747            // Walk up scopes to find the variable declaration
748            find_variable_type(expr, source, var_name)
749        }
750        "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
751        "boolean_literal" => Some("bool".into()),
752        "string_literal" | "hex_string_literal" => Some("string".into()),
753        _ => None,
754    }
755}
756
757/// Find the type of a variable by searching upward through enclosing scopes.
758///
759/// Looks for `parameter`, `variable_declaration`, and `state_variable_declaration`
760/// nodes whose identifier matches the variable name.
761fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
762    let mut scope = from.parent();
763    while let Some(node) = scope {
764        match node.kind() {
765            "function_definition" | "modifier_definition" | "constructor_definition" => {
766                // Check parameters
767                let mut c = node.walk();
768                for child in node.children(&mut c) {
769                    if child.kind() == "parameter"
770                        && let Some(id) = ts_child_id_text(child, source)
771                        && id == var_name
772                    {
773                        // Extract the type from this parameter
774                        let mut pc = child.walk();
775                        return child
776                            .children(&mut pc)
777                            .find(|c| {
778                                matches!(
779                                    c.kind(),
780                                    "type_name"
781                                        | "primitive_type"
782                                        | "user_defined_type"
783                                        | "mapping"
784                                )
785                            })
786                            .map(|t| source[t.byte_range()].trim().to_string());
787                    }
788                }
789            }
790            "function_body" | "block_statement" | "unchecked_block" => {
791                // Check local variable declarations
792                let mut c = node.walk();
793                for child in node.children(&mut c) {
794                    if child.kind() == "variable_declaration_statement"
795                        || child.kind() == "variable_declaration"
796                    {
797                        if let Some(id) = ts_child_id_text(child, source)
798                            && id == var_name
799                        {
800                            let mut pc = child.walk();
801                            return child
802                                .children(&mut pc)
803                                .find(|c| {
804                                    matches!(
805                                        c.kind(),
806                                        "type_name"
807                                            | "primitive_type"
808                                            | "user_defined_type"
809                                            | "mapping"
810                                    )
811                                })
812                                .map(|t| source[t.byte_range()].trim().to_string());
813                        }
814                    }
815                }
816            }
817            "contract_declaration" | "library_declaration" | "interface_declaration" => {
818                // Check state variables
819                if let Some(body) = ts_find_child(node, "contract_body") {
820                    let mut c = body.walk();
821                    for child in body.children(&mut c) {
822                        if child.kind() == "state_variable_declaration"
823                            && let Some(id) = ts_child_id_text(child, source)
824                            && id == var_name
825                        {
826                            let mut pc = child.walk();
827                            return child
828                                .children(&mut pc)
829                                .find(|c| {
830                                    matches!(
831                                        c.kind(),
832                                        "type_name"
833                                            | "primitive_type"
834                                            | "user_defined_type"
835                                            | "mapping"
836                                    )
837                                })
838                                .map(|t| source[t.byte_range()].trim().to_string());
839                        }
840                    }
841                }
842            }
843            _ => {}
844        }
845        scope = node.parent();
846    }
847    None
848}
849
850/// Infer argument types at a call site by examining each `call_argument` child.
851fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
852    let mut cursor = call_node.walk();
853    call_node
854        .children(&mut cursor)
855        .filter(|c| c.kind() == "call_argument")
856        .map(|arg| infer_argument_type(arg, source))
857        .collect()
858}
859
860/// Pick the best overload from multiple declarations based on argument types.
861///
862/// Strategy:
863/// 1. If only one declaration, return it.
864/// 2. Filter by argument count first.
865/// 3. Among count-matched declarations, score by how many argument types match.
866/// 4. Return the highest-scoring declaration.
867fn best_overload<'a>(
868    decls: &'a [TsDeclaration],
869    arg_count: Option<usize>,
870    arg_types: &[Option<String>],
871) -> Option<&'a TsDeclaration> {
872    if decls.len() == 1 {
873        return decls.first();
874    }
875    if decls.is_empty() {
876        return None;
877    }
878
879    // Filter to only function declarations (skip parameters, variables, etc.)
880    let func_decls: Vec<&TsDeclaration> =
881        decls.iter().filter(|d| d.param_count.is_some()).collect();
882
883    if func_decls.is_empty() {
884        return decls.first();
885    }
886
887    // If we have arg_count, filter by it
888    let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
889        let matched: Vec<_> = func_decls
890            .iter()
891            .filter(|d| d.param_count == Some(ac))
892            .collect();
893        if matched.len() == 1 {
894            return Some(matched[0]);
895        }
896        if matched.is_empty() {
897            // No count match — fall back to all
898            func_decls.iter().collect()
899        } else {
900            matched
901        }
902    } else {
903        func_decls.iter().collect()
904    };
905
906    // Score each candidate by how many argument types match parameter types
907    if !arg_types.is_empty() {
908        let mut best: Option<(&TsDeclaration, usize)> = None;
909        for &&decl in &count_matched {
910            let score = arg_types
911                .iter()
912                .zip(decl.param_types.iter())
913                .filter(|(arg_ty, param_ty)| {
914                    if let Some(at) = arg_ty {
915                        at == param_ty.as_str()
916                    } else {
917                        false
918                    }
919                })
920                .count();
921            if best.is_none() || score > best.unwrap().1 {
922                best = Some((decl, score));
923            }
924        }
925        if let Some((decl, _)) = best {
926            return Some(decl);
927        }
928    }
929
930    // Fallback: return first count-matched or first overall
931    count_matched.first().map(|d| **d).or(decls.first())
932}
933
934/// Extract cursor context: the identifier under the cursor and its ancestor names.
935///
936/// Walks up the tree-sitter parse tree to find the enclosing function and contract.
937pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
938    let tree = ts_parse(source)?;
939    let byte = pos_to_bytes(source.as_bytes(), position);
940    let leaf = ts_node_at_byte(tree.root_node(), byte)?;
941
942    // The leaf should be an identifier (or we find the nearest identifier)
943    let id_node = if leaf.kind() == "identifier" {
944        leaf
945    } else {
946        // Check parent — cursor might be just inside a node that contains an identifier
947        let parent = leaf.parent()?;
948        if parent.kind() == "identifier" {
949            parent
950        } else {
951            return None;
952        }
953    };
954
955    let name = source[id_node.byte_range()].to_string();
956    let mut function = None;
957    let mut contract = None;
958
959    // Detect member access: if the identifier is the `property` side of a
960    // member_expression (e.g. `SqrtPriceMath.getAmount0Delta`), extract
961    // the object name so the caller can resolve cross-file.
962    let object = id_node.parent().and_then(|parent| {
963        if parent.kind() == "member_expression" {
964            let prop = parent.child_by_field_name("property")?;
965            // Only set object when cursor is on the property, not the object side
966            if prop.id() == id_node.id() {
967                let obj = parent.child_by_field_name("object")?;
968                Some(source[obj.byte_range()].to_string())
969            } else {
970                None
971            }
972        } else {
973            None
974        }
975    });
976
977    // Count arguments and infer types at the call site for overload disambiguation.
978    // Walk up from the identifier to find an enclosing `call_expression`,
979    // then count its `call_argument` children and infer their types.
980    let (arg_count, arg_types) = {
981        let mut node = id_node.parent();
982        let mut result = (None, vec![]);
983        while let Some(n) = node {
984            if n.kind() == "call_expression" {
985                let types = infer_call_arg_types(n, source);
986                result = (Some(types.len()), types);
987                break;
988            }
989            node = n.parent();
990        }
991        result
992    };
993
994    // Walk ancestors
995    let mut current = id_node.parent();
996    while let Some(node) = current {
997        match node.kind() {
998            "function_definition" | "modifier_definition" if function.is_none() => {
999                function = ts_child_id_text(node, source).map(String::from);
1000            }
1001            "constructor_definition" if function.is_none() => {
1002                function = Some("constructor".into());
1003            }
1004            "contract_declaration" | "interface_declaration" | "library_declaration"
1005                if contract.is_none() =>
1006            {
1007                contract = ts_child_id_text(node, source).map(String::from);
1008            }
1009            _ => {}
1010        }
1011        current = node.parent();
1012    }
1013
1014    Some(CursorContext {
1015        name,
1016        function,
1017        contract,
1018        object,
1019        arg_count,
1020        arg_types,
1021    })
1022}
1023
1024/// Information about a declaration found by tree-sitter.
1025#[derive(Debug, Clone)]
1026pub struct TsDeclaration {
1027    /// Position range of the declaration identifier.
1028    pub range: Range,
1029    /// What kind of declaration (contract, function, state_variable, etc.).
1030    pub kind: &'static str,
1031    /// Container name (contract/struct that owns this declaration).
1032    pub container: Option<String>,
1033    /// Number of parameters (for function/modifier declarations).
1034    pub param_count: Option<usize>,
1035    /// Parameter type signature (e.g. `["uint160", "uint160", "int128"]`).
1036    /// Used for overload disambiguation.
1037    pub param_types: Vec<String>,
1038}
1039
1040/// Find all declarations of a name in a source file using tree-sitter.
1041///
1042/// Scans the parse tree for declaration nodes (state variables, functions, events,
1043/// errors, structs, enums, contracts, etc.) whose identifier matches `name`.
1044pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1045    let tree = match ts_parse(source) {
1046        Some(t) => t,
1047        None => return vec![],
1048    };
1049    let mut results = Vec::new();
1050    collect_declarations(tree.root_node(), source, name, None, &mut results);
1051    results
1052}
1053
1054fn collect_declarations(
1055    node: Node,
1056    source: &str,
1057    name: &str,
1058    container: Option<&str>,
1059    out: &mut Vec<TsDeclaration>,
1060) {
1061    let mut cursor = node.walk();
1062    for child in node.children(&mut cursor) {
1063        if !child.is_named() {
1064            continue;
1065        }
1066        match child.kind() {
1067            "contract_declaration" | "interface_declaration" | "library_declaration" => {
1068                if let Some(id_name) = ts_child_id_text(child, source) {
1069                    if id_name == name {
1070                        out.push(TsDeclaration {
1071                            range: id_range(child),
1072                            kind: child.kind(),
1073                            container: container.map(String::from),
1074                            param_count: None,
1075                            param_types: vec![],
1076                        });
1077                    }
1078                    // Recurse into contract body
1079                    if let Some(body) = ts_find_child(child, "contract_body") {
1080                        collect_declarations(body, source, name, Some(id_name), out);
1081                    }
1082                }
1083            }
1084            "function_definition" | "modifier_definition" => {
1085                if let Some(id_name) = ts_child_id_text(child, source) {
1086                    if id_name == name {
1087                        let types = parameter_type_signature(child, source);
1088                        out.push(TsDeclaration {
1089                            range: id_range(child),
1090                            kind: child.kind(),
1091                            container: container.map(String::from),
1092                            param_count: Some(types.len()),
1093                            param_types: types.into_iter().map(String::from).collect(),
1094                        });
1095                    }
1096                    // Check function parameters
1097                    collect_parameters(child, source, name, container, out);
1098                    // Recurse into function body for local variables
1099                    if let Some(body) = ts_find_child(child, "function_body") {
1100                        collect_declarations(body, source, name, container, out);
1101                    }
1102                }
1103            }
1104            "constructor_definition" => {
1105                if name == "constructor" {
1106                    let types = parameter_type_signature(child, source);
1107                    out.push(TsDeclaration {
1108                        range: ts_range(child),
1109                        kind: "constructor_definition",
1110                        container: container.map(String::from),
1111                        param_count: Some(types.len()),
1112                        param_types: types.into_iter().map(String::from).collect(),
1113                    });
1114                }
1115                // Check constructor parameters
1116                collect_parameters(child, source, name, container, out);
1117                if let Some(body) = ts_find_child(child, "function_body") {
1118                    collect_declarations(body, source, name, container, out);
1119                }
1120            }
1121            "state_variable_declaration" | "variable_declaration" => {
1122                if let Some(id_name) = ts_child_id_text(child, source)
1123                    && id_name == name
1124                {
1125                    out.push(TsDeclaration {
1126                        range: id_range(child),
1127                        kind: child.kind(),
1128                        container: container.map(String::from),
1129                        param_count: None,
1130                        param_types: vec![],
1131                    });
1132                }
1133            }
1134            "struct_declaration" => {
1135                if let Some(id_name) = ts_child_id_text(child, source) {
1136                    if id_name == name {
1137                        out.push(TsDeclaration {
1138                            range: id_range(child),
1139                            kind: "struct_declaration",
1140                            container: container.map(String::from),
1141                            param_count: None,
1142                            param_types: vec![],
1143                        });
1144                    }
1145                    if let Some(body) = ts_find_child(child, "struct_body") {
1146                        collect_declarations(body, source, name, Some(id_name), out);
1147                    }
1148                }
1149            }
1150            "enum_declaration" => {
1151                if let Some(id_name) = ts_child_id_text(child, source) {
1152                    if id_name == name {
1153                        out.push(TsDeclaration {
1154                            range: id_range(child),
1155                            kind: "enum_declaration",
1156                            container: container.map(String::from),
1157                            param_count: None,
1158                            param_types: vec![],
1159                        });
1160                    }
1161                    // Check enum values
1162                    if let Some(body) = ts_find_child(child, "enum_body") {
1163                        let mut ecur = body.walk();
1164                        for val in body.children(&mut ecur) {
1165                            if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1166                                out.push(TsDeclaration {
1167                                    range: ts_range(val),
1168                                    kind: "enum_value",
1169                                    container: Some(id_name.to_string()),
1170                                    param_count: None,
1171                                    param_types: vec![],
1172                                });
1173                            }
1174                        }
1175                    }
1176                }
1177            }
1178            "event_definition" | "error_declaration" => {
1179                if let Some(id_name) = ts_child_id_text(child, source)
1180                    && id_name == name
1181                {
1182                    out.push(TsDeclaration {
1183                        range: id_range(child),
1184                        kind: child.kind(),
1185                        container: container.map(String::from),
1186                        param_count: None,
1187                        param_types: vec![],
1188                    });
1189                }
1190            }
1191            "user_defined_type_definition" => {
1192                if let Some(id_name) = ts_child_id_text(child, source)
1193                    && id_name == name
1194                {
1195                    out.push(TsDeclaration {
1196                        range: id_range(child),
1197                        kind: "user_defined_type_definition",
1198                        container: container.map(String::from),
1199                        param_count: None,
1200                        param_types: vec![],
1201                    });
1202                }
1203            }
1204            // Recurse into blocks, if-else, loops, etc.
1205            _ => {
1206                collect_declarations(child, source, name, container, out);
1207            }
1208        }
1209    }
1210}
1211
1212/// Extract the type signature from a function's parameters.
1213///
1214/// Returns a list of type strings, e.g. `["uint160", "uint160", "int128"]`.
1215/// For complex types (mappings, arrays, user-defined), returns the full
1216/// text of the type node.
1217fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1218    let mut cursor = node.walk();
1219    node.children(&mut cursor)
1220        .filter(|c| c.kind() == "parameter")
1221        .filter_map(|param| {
1222            let mut pc = param.walk();
1223            param
1224                .children(&mut pc)
1225                .find(|c| {
1226                    matches!(
1227                        c.kind(),
1228                        "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1229                    )
1230                })
1231                .map(|t| source[t.byte_range()].trim())
1232        })
1233        .collect()
1234}
1235
1236/// Collect parameter declarations from a function/constructor node.
1237fn collect_parameters(
1238    node: Node,
1239    source: &str,
1240    name: &str,
1241    container: Option<&str>,
1242    out: &mut Vec<TsDeclaration>,
1243) {
1244    let mut cursor = node.walk();
1245    for child in node.children(&mut cursor) {
1246        if child.kind() == "parameter"
1247            && let Some(id_name) = ts_child_id_text(child, source)
1248            && id_name == name
1249        {
1250            out.push(TsDeclaration {
1251                range: id_range(child),
1252                kind: "parameter",
1253                container: container.map(String::from),
1254                param_count: None,
1255                param_types: vec![],
1256            });
1257        }
1258    }
1259}
1260
1261/// Tree-sitter range helper.
1262fn ts_range(node: Node) -> Range {
1263    let s = node.start_position();
1264    let e = node.end_position();
1265    Range {
1266        start: Position::new(s.row as u32, s.column as u32),
1267        end: Position::new(e.row as u32, e.column as u32),
1268    }
1269}
1270
1271/// Get the range of the identifier child within a declaration node.
1272fn id_range(node: Node) -> Range {
1273    let mut cursor = node.walk();
1274    node.children(&mut cursor)
1275        .find(|c| c.kind() == "identifier" && c.is_named())
1276        .map(|c| ts_range(c))
1277        .unwrap_or_else(|| ts_range(node))
1278}
1279
1280fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1281    let mut cursor = node.walk();
1282    node.children(&mut cursor).find(|c| c.kind() == kind)
1283}
1284
1285/// Tree-sitter enhanced goto definition.
1286///
1287/// Uses tree-sitter to find the identifier under the cursor and its scope,
1288/// then resolves via the CompletionCache (for cross-file/semantic resolution),
1289/// and finally uses tree-sitter to find the declaration position in the target file.
1290///
1291/// Falls back to None if resolution fails — caller should try the existing AST-based path.
1292pub fn goto_definition_ts(
1293    source: &str,
1294    position: Position,
1295    file_uri: &Url,
1296    completion_cache: &crate::completion::CompletionCache,
1297    text_cache: &HashMap<String, (i32, String)>,
1298) -> Option<Location> {
1299    let ctx = cursor_context(source, position)?;
1300
1301    // Member access: cursor is on `getAmount0Delta` in `SqrtPriceMath.getAmount0Delta`.
1302    // Look up the object (SqrtPriceMath) in the completion cache to find its file,
1303    // then search that file for the member declaration.
1304    // When multiple overloads exist, disambiguate by argument count and types.
1305    if let Some(obj_name) = &ctx.object {
1306        if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1307            let target_source = read_target_source(&path, text_cache)?;
1308            let target_uri = Url::from_file_path(&path).ok()?;
1309            let decls = find_declarations_by_name(&target_source, &ctx.name);
1310            if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1311                return Some(Location {
1312                    uri: target_uri,
1313                    range: d.range,
1314                });
1315            }
1316        }
1317        // Object might be in the same file (e.g. a struct or contract in this file)
1318        let decls = find_declarations_by_name(source, &ctx.name);
1319        if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1320            return Some(Location {
1321                uri: file_uri.clone(),
1322                range: d.range,
1323            });
1324        }
1325    }
1326
1327    // Step 1: Try to resolve via CompletionCache to find which file + name the declaration is in.
1328    // Use the scope chain by names: find the contract scope, then resolve the name.
1329    let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1330
1331    match resolved {
1332        Some(ResolvedTarget::SameFile) => {
1333            // Declaration is in the same file — find it with tree-sitter
1334            find_best_declaration(source, &ctx, file_uri)
1335        }
1336        Some(ResolvedTarget::OtherFile { path, name }) => {
1337            // Declaration is in another file — read target source and find by name
1338            let target_source = read_target_source(&path, text_cache);
1339            let target_source = target_source?;
1340            let target_uri = Url::from_file_path(&path).ok()?;
1341            let decls = find_declarations_by_name(&target_source, &name);
1342            decls.first().map(|d| Location {
1343                uri: target_uri,
1344                range: d.range,
1345            })
1346        }
1347        None => {
1348            // CompletionCache couldn't resolve — try same-file tree-sitter lookup as fallback
1349            find_best_declaration(source, &ctx, file_uri)
1350        }
1351    }
1352}
1353
1354#[derive(Debug)]
1355enum ResolvedTarget {
1356    /// Declaration is in the same file as the usage.
1357    SameFile,
1358    /// Declaration is in a different file.
1359    OtherFile { path: String, name: String },
1360}
1361
1362/// Try to resolve an identifier using the CompletionCache.
1363///
1364/// Finds the scope by matching ancestor names (contract, function) against
1365/// the cache's scope data, then resolves the name to a type and traces
1366/// back to the declaring file.
1367fn resolve_via_cache(
1368    ctx: &CursorContext,
1369    file_uri: &Url,
1370    cache: &crate::completion::CompletionCache,
1371) -> Option<ResolvedTarget> {
1372    // Find the contract scope node_id by name
1373    let contract_scope = ctx
1374        .contract
1375        .as_ref()
1376        .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1377        .copied();
1378
1379    // Try scope-based resolution: look in the contract's scope_declarations
1380    if let Some(contract_id) = contract_scope {
1381        // Check function scope if we're inside one
1382        if let Some(func_name) = &ctx.function {
1383            // Find the function scope: look for a scope whose parent is this contract
1384            // and which has a declaration for this function name
1385            if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1386                // Check declarations in this function scope first
1387                if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1388                    && decls.iter().any(|d| d.name == ctx.name)
1389                {
1390                    return Some(ResolvedTarget::SameFile);
1391                }
1392            }
1393        }
1394
1395        // Check contract scope declarations (state variables, functions)
1396        if let Some(decls) = cache.scope_declarations.get(&contract_id)
1397            && decls.iter().any(|d| d.name == ctx.name)
1398        {
1399            return Some(ResolvedTarget::SameFile);
1400        }
1401
1402        // Check inherited contracts (C3 linearization)
1403        if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1404            for &base_id in bases.iter().skip(1) {
1405                if let Some(decls) = cache.scope_declarations.get(&base_id)
1406                    && decls.iter().any(|d| d.name == ctx.name)
1407                {
1408                    // Found in a base contract — find which file it's in
1409                    // Reverse lookup: base_id → contract name → file
1410                    let base_name = cache
1411                        .name_to_node_id
1412                        .iter()
1413                        .find(|&(_, &id)| id == base_id)
1414                        .map(|(name, _)| name.clone());
1415
1416                    if let Some(base_name) = base_name
1417                        && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1418                    {
1419                        return Some(ResolvedTarget::OtherFile {
1420                            path,
1421                            name: ctx.name.clone(),
1422                        });
1423                    }
1424                    // Base contract might be in the same file
1425                    return Some(ResolvedTarget::SameFile);
1426                }
1427            }
1428        }
1429    }
1430
1431    // Check if the name is a contract/library/interface name
1432    if cache.name_to_node_id.contains_key(&ctx.name) {
1433        // Could be same file or different file — check if it's in the current file
1434        if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1435            let current_path = file_uri.to_file_path().ok()?;
1436            let current_str = current_path.to_str()?;
1437            if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1438                return Some(ResolvedTarget::SameFile);
1439            }
1440            return Some(ResolvedTarget::OtherFile {
1441                path,
1442                name: ctx.name.clone(),
1443            });
1444        }
1445        return Some(ResolvedTarget::SameFile);
1446    }
1447
1448    // Flat fallback — name_to_type knows about it but we can't determine the file
1449    if cache.name_to_type.contains_key(&ctx.name) {
1450        return Some(ResolvedTarget::SameFile);
1451    }
1452
1453    None
1454}
1455
1456/// Find the scope node_id for a function within a contract.
1457fn find_function_scope(
1458    cache: &crate::completion::CompletionCache,
1459    contract_id: NodeId,
1460    func_name: &str,
1461) -> Option<NodeId> {
1462    // Look for a scope whose parent is the contract and which is a function scope.
1463    // The function name should appear as a declaration in the contract scope,
1464    // and the function's own scope is the one whose parent is the contract.
1465    for (&scope_id, &parent_id) in &cache.scope_parent {
1466        if parent_id == contract_id {
1467            // This scope's parent is our contract — it might be a function scope.
1468            // Check if this scope has declarations (functions/blocks do).
1469            // We also check if the contract declares a function with this name.
1470            if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1471                && contract_decls.iter().any(|d| d.name == func_name)
1472            {
1473                // Found a child scope of the contract — could be the function.
1474                // Check if this scope_id has child scopes or declarations
1475                // that match what we'd expect for a function body.
1476                if cache.scope_declarations.contains_key(&scope_id)
1477                    || cache.scope_parent.values().any(|&p| p == scope_id)
1478                {
1479                    return Some(scope_id);
1480                }
1481            }
1482        }
1483    }
1484    None
1485}
1486
1487/// Find the file path for a contract by searching the CompletionCache's path_to_file_id.
1488fn find_file_for_contract(
1489    cache: &crate::completion::CompletionCache,
1490    contract_name: &str,
1491    _file_uri: &Url,
1492) -> Option<String> {
1493    // The completion cache doesn't directly map contract → file.
1494    // But scope_ranges + path_to_file_id can help.
1495    // For now, check if the contract's node_id appears in any scope_range,
1496    // then map file_id back to path.
1497    let node_id = cache.name_to_node_id.get(contract_name)?;
1498    let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1499    let file_id = scope_range.file_id;
1500
1501    // Reverse lookup: file_id → path
1502    cache
1503        .path_to_file_id
1504        .iter()
1505        .find(|&(_, &fid)| fid == file_id)
1506        .map(|(path, _)| path.clone())
1507}
1508
1509/// Read source for a target file — prefer text_cache (open buffers), fallback to disk.
1510fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1511    // Try text_cache by URI
1512    let uri = Url::from_file_path(path).ok()?;
1513    if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1514        return Some(content.clone());
1515    }
1516    // Fallback to disk
1517    std::fs::read_to_string(path).ok()
1518}
1519
1520/// Find the best matching declaration in the same file.
1521fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1522    let decls = find_declarations_by_name(source, &ctx.name);
1523    if decls.is_empty() {
1524        return None;
1525    }
1526
1527    // If there's only one declaration, use it
1528    if decls.len() == 1 {
1529        return Some(Location {
1530            uri: file_uri.clone(),
1531            range: decls[0].range,
1532        });
1533    }
1534
1535    // Multiple declarations — prefer the one in the same contract
1536    if let Some(contract_name) = &ctx.contract
1537        && let Some(d) = decls
1538            .iter()
1539            .find(|d| d.container.as_deref() == Some(contract_name))
1540    {
1541        return Some(Location {
1542            uri: file_uri.clone(),
1543            range: d.range,
1544        });
1545    }
1546
1547    // Fallback: return first declaration
1548    Some(Location {
1549        uri: file_uri.clone(),
1550        range: decls[0].range,
1551    })
1552}
1553
1554#[cfg(test)]
1555mod ts_tests {
1556    use super::*;
1557
1558    #[test]
1559    fn test_cursor_context_state_var() {
1560        let source = r#"
1561contract Token {
1562    uint256 public totalSupply;
1563    function mint(uint256 amount) public {
1564        totalSupply += amount;
1565    }
1566}
1567"#;
1568        // Cursor on `totalSupply` inside mint (line 4, col 8)
1569        let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
1570        assert_eq!(ctx.name, "totalSupply");
1571        assert_eq!(ctx.function.as_deref(), Some("mint"));
1572        assert_eq!(ctx.contract.as_deref(), Some("Token"));
1573    }
1574
1575    #[test]
1576    fn test_cursor_context_top_level() {
1577        let source = r#"
1578contract Foo {}
1579contract Bar {}
1580"#;
1581        // Cursor on `Foo` (line 1, col 9) — the identifier of the contract declaration
1582        let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
1583        assert_eq!(ctx.name, "Foo");
1584        assert!(ctx.function.is_none());
1585        // The identifier `Foo` is a child of contract_declaration, so contract is set
1586        assert_eq!(ctx.contract.as_deref(), Some("Foo"));
1587    }
1588
1589    #[test]
1590    fn test_find_declarations() {
1591        let source = r#"
1592contract Token {
1593    uint256 public totalSupply;
1594    function mint(uint256 amount) public {
1595        totalSupply += amount;
1596    }
1597}
1598"#;
1599        let decls = find_declarations_by_name(source, "totalSupply");
1600        assert_eq!(decls.len(), 1);
1601        assert_eq!(decls[0].kind, "state_variable_declaration");
1602        assert_eq!(decls[0].container.as_deref(), Some("Token"));
1603    }
1604
1605    #[test]
1606    fn test_find_declarations_multiple_contracts() {
1607        let source = r#"
1608contract A {
1609    uint256 public value;
1610}
1611contract B {
1612    uint256 public value;
1613}
1614"#;
1615        let decls = find_declarations_by_name(source, "value");
1616        assert_eq!(decls.len(), 2);
1617        assert_eq!(decls[0].container.as_deref(), Some("A"));
1618        assert_eq!(decls[1].container.as_deref(), Some("B"));
1619    }
1620
1621    #[test]
1622    fn test_find_declarations_enum_value() {
1623        let source = "contract Foo { enum Status { Active, Paused } }";
1624        let decls = find_declarations_by_name(source, "Active");
1625        assert_eq!(decls.len(), 1);
1626        assert_eq!(decls[0].kind, "enum_value");
1627        assert_eq!(decls[0].container.as_deref(), Some("Status"));
1628    }
1629
1630    #[test]
1631    fn test_cursor_context_short_param() {
1632        let source = r#"
1633contract Shop {
1634    uint256 public TAX;
1635    constructor(uint256 price, uint16 tax, uint16 taxBase) {
1636        TAX = tax;
1637    }
1638}
1639"#;
1640        // Cursor on `tax` usage at line 4, col 14 (TAX = tax;)
1641        let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
1642        assert_eq!(ctx.name, "tax");
1643        assert_eq!(ctx.contract.as_deref(), Some("Shop"));
1644
1645        // Cursor on `TAX` at line 4, col 8
1646        let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
1647        assert_eq!(ctx2.name, "TAX");
1648
1649        // Parameters are found as declarations
1650        let decls = find_declarations_by_name(source, "tax");
1651        assert_eq!(decls.len(), 1);
1652        assert_eq!(decls[0].kind, "parameter");
1653
1654        let decls_tax_base = find_declarations_by_name(source, "taxBase");
1655        assert_eq!(decls_tax_base.len(), 1);
1656        assert_eq!(decls_tax_base[0].kind, "parameter");
1657
1658        let decls_price = find_declarations_by_name(source, "price");
1659        assert_eq!(decls_price.len(), 1);
1660        assert_eq!(decls_price[0].kind, "parameter");
1661
1662        // State variable is also found
1663        let decls_tax_upper = find_declarations_by_name(source, "TAX");
1664        assert_eq!(decls_tax_upper.len(), 1);
1665        assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
1666    }
1667
1668    #[test]
1669    fn test_find_best_declaration_same_contract() {
1670        let source = r#"
1671contract A { uint256 public x; }
1672contract B { uint256 public x; }
1673"#;
1674        let ctx = CursorContext {
1675            name: "x".into(),
1676            function: None,
1677            contract: Some("B".into()),
1678            object: None,
1679            arg_count: None,
1680            arg_types: vec![],
1681        };
1682        let uri = Url::parse("file:///test.sol").unwrap();
1683        let loc = find_best_declaration(source, &ctx, &uri).unwrap();
1684        // Should pick B's x (line 2), not A's x (line 1)
1685        assert_eq!(loc.range.start.line, 2);
1686    }
1687}