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