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