Skip to main content

solidity_language_server/
goto.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use tower_lsp::lsp_types::{Location, Position, Range, TextEdit, Url};
5use tree_sitter::{Node, Parser};
6
7use crate::types::{NodeId, SourceLoc};
8use crate::utils::push_if_node_or_array;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct NodeInfo {
12    pub src: String,
13    pub name_location: Option<String>,
14    pub name_locations: Vec<String>,
15    pub referenced_declaration: Option<NodeId>,
16    pub node_type: Option<String>,
17    pub member_location: Option<String>,
18    pub absolute_path: Option<String>,
19}
20
21/// All AST child keys to traverse (Solidity + Yul).
22pub const CHILD_KEYS: &[&str] = &[
23    "AST",
24    "arguments",
25    "baseContracts",
26    "baseExpression",
27    "baseName",
28    "baseType",
29    "block",
30    "body",
31    "components",
32    "condition",
33    "declarations",
34    "endExpression",
35    "errorCall",
36    "eventCall",
37    "expression",
38    "externalCall",
39    "falseBody",
40    "falseExpression",
41    "file",
42    "foreign",
43    "functionName",
44    "indexExpression",
45    "initialValue",
46    "initializationExpression",
47    "keyType",
48    "leftExpression",
49    "leftHandSide",
50    "libraryName",
51    "literals",
52    "loopExpression",
53    "members",
54    "modifierName",
55    "modifiers",
56    "name",
57    "names",
58    "nodes",
59    "options",
60    "overrides",
61    "parameters",
62    "pathNode",
63    "post",
64    "pre",
65    "returnParameters",
66    "rightExpression",
67    "rightHandSide",
68    "startExpression",
69    "statements",
70    "storageLayout",
71    "subExpression",
72    "subdenomination",
73    "symbolAliases",
74    "trueBody",
75    "trueExpression",
76    "typeName",
77    "unitAlias",
78    "value",
79    "valueType",
80    "variableNames",
81    "variables",
82];
83
84/// Maps `"offset:length:fileId"` src strings from Yul externalReferences
85/// to the Solidity declaration node id they refer to.
86pub type ExternalRefs = HashMap<String, NodeId>;
87
88/// Pre-computed AST index. Built once when an AST enters the cache,
89/// then reused on every goto/references/rename/hover request.
90///
91/// All data from the raw solc JSON is consumed during `new()` into
92/// pre-built indexes. The raw JSON is not retained.
93#[derive(Debug, Clone)]
94pub struct CachedBuild {
95    pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
96    pub path_to_abs: HashMap<String, String>,
97    pub external_refs: ExternalRefs,
98    pub id_to_path_map: HashMap<String, String>,
99    /// O(1) typed declaration node lookup by AST node ID.
100    /// Built from the typed AST via visitor. Contains functions, variables,
101    /// contracts, events, errors, structs, enums, modifiers, and UDVTs.
102    pub decl_index: HashMap<i64, crate::solc_ast::DeclNode>,
103    /// O(1) lookup from any declaration/source-unit node ID to its source file path.
104    /// Built from `typed_ast` during construction. Replaces the O(N)
105    /// `find_source_path_for_node()` that walked raw JSON.
106    pub node_id_to_source_path: HashMap<i64, String>,
107    /// Pre-built gas index from contract output. Built once, reused by
108    /// hover, inlay hints, and code lens.
109    pub gas_index: crate::gas::GasIndex,
110    /// Pre-built hint lookup per file. Built once, reused on every
111    /// inlay hint request (avoids O(n²) declaration resolution per request).
112    pub hint_index: crate::inlay_hints::HintIndex,
113    /// Pre-built documentation index from solc userdoc/devdoc.
114    /// Merged and keyed by selector for fast hover lookup.
115    pub doc_index: crate::hover::DocIndex,
116    /// Pre-built completion cache. Built from sources during construction
117    /// before the sources key is stripped.
118    pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
119    /// The text_cache version this build was created from.
120    /// Used to detect dirty files (unsaved edits since last build).
121    pub build_version: i32,
122    /// FxHash of the source text this build was compiled from.
123    /// Used to skip redundant rebuilds when content has not changed
124    /// (e.g. format-on-save loops that re-trigger didSave with identical text).
125    pub content_hash: u64,
126}
127
128impl CachedBuild {
129    /// Build the index from normalized AST output.
130    ///
131    /// Canonical shape:
132    /// - `sources[path] = { id, ast }`
133    /// - `contracts[path][name] = { abi, evm, ... }`
134    /// - `source_id_to_path = { "0": "path", ... }`
135    pub fn new(ast: Value, build_version: i32) -> Self {
136        let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
137            cache_ids(sources)
138        } else {
139            (HashMap::new(), HashMap::new(), HashMap::new())
140        };
141
142        let id_to_path_map = ast
143            .get("source_id_to_path")
144            .and_then(|v| v.as_object())
145            .map(|obj| {
146                obj.iter()
147                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
148                    .collect()
149            })
150            .unwrap_or_default();
151
152        let gas_index = crate::gas::build_gas_index(&ast);
153
154        let doc_index = crate::hover::build_doc_index(&ast);
155
156        // Extract declaration nodes directly from the raw sources JSON.
157        // Instead of deserializing the entire typed AST (SourceUnit, all
158        // expressions, statements, Yul blocks), this walks the raw Value
159        // tree and only deserializes nodes whose nodeType matches one of the
160        // 9 declaration types. Heavy fields (body, modifiers, value, etc.)
161        // are stripped before deserialization.
162        let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
163            match crate::solc_ast::extract_decl_nodes(sources) {
164                Some(extracted) => (extracted.decl_index, extracted.node_id_to_source_path),
165                None => (HashMap::new(), HashMap::new()),
166            }
167        } else {
168            (HashMap::new(), HashMap::new())
169        };
170
171        // Build constructor index and hint index from the typed decl_index.
172        let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
173        let hint_index = if let Some(sources) = ast.get("sources") {
174            crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
175        } else {
176            HashMap::new()
177        };
178
179        // Build completion cache before stripping sources.
180        let completion_cache = {
181            let sources = ast.get("sources");
182            let contracts = ast.get("contracts");
183            let cc = if let Some(s) = sources {
184                crate::completion::build_completion_cache(s, contracts)
185            } else {
186                crate::completion::build_completion_cache(
187                    &serde_json::Value::Object(Default::default()),
188                    contracts,
189                )
190            };
191            std::sync::Arc::new(cc)
192        };
193
194        // The raw AST JSON is fully consumed — all data has been extracted
195        // into the pre-built indexes above. `ast` is dropped here.
196
197        Self {
198            nodes,
199            path_to_abs,
200            external_refs,
201            id_to_path_map,
202            decl_index,
203            node_id_to_source_path,
204            gas_index,
205            hint_index,
206            doc_index,
207            completion_cache,
208            build_version,
209            content_hash: 0,
210        }
211    }
212
213    /// Absorb data from a previous build for files this build doesn't cover.
214    ///
215    /// For each file in `other.nodes` that is **not** already present in
216    /// `self.nodes`, copies the node map, path mapping, and any related
217    /// entries.  This ensures a freshly compiled project index never loses
218    /// coverage compared to the warm-loaded cache it replaces.
219    pub fn merge_missing_from(&mut self, other: &CachedBuild) {
220        for (abs_path, file_nodes) in &other.nodes {
221            if !self.nodes.contains_key(abs_path) {
222                self.nodes.insert(abs_path.clone(), file_nodes.clone());
223            }
224        }
225        for (k, v) in &other.path_to_abs {
226            self.path_to_abs
227                .entry(k.clone())
228                .or_insert_with(|| v.clone());
229        }
230        for (k, v) in &other.external_refs {
231            self.external_refs.entry(k.clone()).or_insert(*v);
232        }
233        for (k, v) in &other.id_to_path_map {
234            self.id_to_path_map
235                .entry(k.clone())
236                .or_insert_with(|| v.clone());
237        }
238    }
239
240    /// Construct a minimal cached build from persisted reference/goto indexes.
241    ///
242    /// This is used for fast startup warm-cache restores where we only need
243    /// cross-file node/reference maps (not full gas/doc/hint indexes).
244    pub fn from_reference_index(
245        nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
246        path_to_abs: HashMap<String, String>,
247        external_refs: ExternalRefs,
248        id_to_path_map: HashMap<String, String>,
249        build_version: i32,
250    ) -> Self {
251        let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
252            &serde_json::Value::Object(Default::default()),
253            None,
254        ));
255
256        Self {
257            nodes,
258            path_to_abs,
259            external_refs,
260            id_to_path_map,
261            decl_index: HashMap::new(),
262            node_id_to_source_path: HashMap::new(),
263            gas_index: HashMap::new(),
264            hint_index: HashMap::new(),
265            doc_index: HashMap::new(),
266            completion_cache,
267            build_version,
268            content_hash: 0,
269        }
270    }
271}
272
273/// Return type of [`cache_ids`]: `(nodes, path_to_abs, external_refs)`.
274type CachedIds = (
275    HashMap<String, HashMap<NodeId, NodeInfo>>,
276    HashMap<String, String>,
277    ExternalRefs,
278);
279
280pub fn cache_ids(sources: &Value) -> CachedIds {
281    let source_count = sources.as_object().map_or(0, |obj| obj.len());
282
283    // Pre-size top-level maps based on source file count to avoid rehashing.
284    // Typical project: ~200 nodes/file, ~10 external refs/file.
285    let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> =
286        HashMap::with_capacity(source_count);
287    let mut path_to_abs: HashMap<String, String> = HashMap::with_capacity(source_count);
288    let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
289
290    if let Some(sources_obj) = sources.as_object() {
291        for (path, source_data) in sources_obj {
292            if let Some(ast) = source_data.get("ast") {
293                // Get the absolute path for this file
294                let abs_path = ast
295                    .get("absolutePath")
296                    .and_then(|v| v.as_str())
297                    .unwrap_or(path)
298                    .to_string();
299
300                path_to_abs.insert(path.clone(), abs_path.clone());
301
302                // Initialize the per-file node map with a size hint.
303                // Use the top-level `nodes` array length as a proxy for total
304                // AST node count (actual count is higher due to nesting, but
305                // this avoids the first few rehashes).
306                let size_hint = ast
307                    .get("nodes")
308                    .and_then(|v| v.as_array())
309                    .map_or(64, |arr| arr.len() * 8);
310                if !nodes.contains_key(&abs_path) {
311                    nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
312                }
313
314                if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
315                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
316                {
317                    nodes.get_mut(&abs_path).unwrap().insert(
318                        NodeId(id),
319                        NodeInfo {
320                            src: src.to_string(),
321                            name_location: None,
322                            name_locations: vec![],
323                            referenced_declaration: None,
324                            node_type: ast
325                                .get("nodeType")
326                                .and_then(|v| v.as_str())
327                                .map(|s| s.to_string()),
328                            member_location: None,
329                            absolute_path: ast
330                                .get("absolutePath")
331                                .and_then(|v| v.as_str())
332                                .map(|s| s.to_string()),
333                        },
334                    );
335                }
336
337                let mut stack = vec![ast];
338
339                while let Some(tree) = stack.pop() {
340                    if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
341                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
342                    {
343                        let id = NodeId(raw_id);
344                        // Check for nameLocation first
345                        let mut name_location = tree
346                            .get("nameLocation")
347                            .and_then(|v| v.as_str())
348                            .map(|s| s.to_string());
349
350                        // Check for nameLocations array and use appropriate element
351                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
352                        // For other nodes, use the first element
353                        if name_location.is_none()
354                            && let Some(name_locations) = tree.get("nameLocations")
355                            && let Some(locations_array) = name_locations.as_array()
356                            && !locations_array.is_empty()
357                        {
358                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
359                            if node_type == Some("IdentifierPath") {
360                                name_location = locations_array
361                                    .last()
362                                    .and_then(|v| v.as_str())
363                                    .map(|s| s.to_string());
364                            } else {
365                                name_location = locations_array[0].as_str().map(|s| s.to_string());
366                            }
367                        }
368
369                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
370                            && let Some(locations_array) = name_locations.as_array()
371                        {
372                            locations_array
373                                .iter()
374                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
375                                .collect()
376                        } else {
377                            vec![]
378                        };
379
380                        let mut final_name_location = name_location;
381                        if final_name_location.is_none()
382                            && let Some(member_loc) =
383                                tree.get("memberLocation").and_then(|v| v.as_str())
384                        {
385                            final_name_location = Some(member_loc.to_string());
386                        }
387
388                        let node_info = NodeInfo {
389                            src: src.to_string(),
390                            name_location: final_name_location,
391                            name_locations,
392                            referenced_declaration: tree
393                                .get("referencedDeclaration")
394                                .and_then(|v| v.as_u64())
395                                .map(NodeId),
396                            node_type: tree
397                                .get("nodeType")
398                                .and_then(|v| v.as_str())
399                                .map(|s| s.to_string()),
400                            member_location: tree
401                                .get("memberLocation")
402                                .and_then(|v| v.as_str())
403                                .map(|s| s.to_string()),
404                            absolute_path: tree
405                                .get("absolutePath")
406                                .and_then(|v| v.as_str())
407                                .map(|s| s.to_string()),
408                        };
409
410                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
411
412                        // Collect externalReferences from InlineAssembly nodes
413                        if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
414                            && let Some(ext_refs) =
415                                tree.get("externalReferences").and_then(|v| v.as_array())
416                        {
417                            for ext_ref in ext_refs {
418                                if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
419                                    && let Some(decl_id) =
420                                        ext_ref.get("declaration").and_then(|v| v.as_u64())
421                                {
422                                    external_refs.insert(src_str.to_string(), NodeId(decl_id));
423                                }
424                            }
425                        }
426                    }
427
428                    for key in CHILD_KEYS {
429                        push_if_node_or_array(tree, key, &mut stack);
430                    }
431                }
432            }
433        }
434    }
435
436    (nodes, path_to_abs, external_refs)
437}
438
439pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
440    let text = String::from_utf8_lossy(source_bytes);
441    crate::utils::position_to_byte_offset(&text, position)
442}
443
444pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
445    let text = String::from_utf8_lossy(source_bytes);
446    let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
447    Some(pos)
448}
449
450/// Convert a `"offset:length:fileId"` src string to an LSP Location.
451pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
452    let loc = SourceLoc::parse(src)?;
453    let file_path = id_to_path.get(&loc.file_id_str())?;
454
455    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
456        std::path::PathBuf::from(file_path)
457    } else {
458        std::env::current_dir().ok()?.join(file_path)
459    };
460
461    let source_bytes = std::fs::read(&absolute_path).ok()?;
462    let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
463    let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
464    let uri = Url::from_file_path(&absolute_path).ok()?;
465
466    Some(Location {
467        uri,
468        range: Range {
469            start: start_pos,
470            end: end_pos,
471        },
472    })
473}
474
475pub fn goto_bytes(
476    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
477    path_to_abs: &HashMap<String, String>,
478    id_to_path: &HashMap<String, String>,
479    external_refs: &ExternalRefs,
480    uri: &str,
481    position: usize,
482) -> Option<(String, usize, usize)> {
483    let path = match uri.starts_with("file://") {
484        true => &uri[7..],
485        false => uri,
486    };
487
488    // Get absolute path for this file
489    let abs_path = path_to_abs.get(path)?;
490
491    // Get nodes for the current file only
492    let current_file_nodes = nodes.get(abs_path)?;
493
494    // Build reverse map: file_path -> file_id for filtering external refs by current file
495    let path_to_file_id: HashMap<&str, &str> = id_to_path
496        .iter()
497        .map(|(id, p)| (p.as_str(), id.as_str()))
498        .collect();
499
500    // Determine the file id for the current file
501    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
502    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
503    let current_file_id = path_to_file_id.get(abs_path.as_str());
504
505    // Check if cursor is on a Yul external reference first
506    for (src_str, decl_id) in external_refs {
507        let Some(src_loc) = SourceLoc::parse(src_str) else {
508            continue;
509        };
510
511        // Only consider external refs in the current file
512        if let Some(file_id) = current_file_id {
513            if src_loc.file_id_str() != *file_id {
514                continue;
515            }
516        } else {
517            continue;
518        }
519
520        if src_loc.offset <= position && position < src_loc.end() {
521            // Found a Yul external reference — resolve to the declaration target
522            let mut target_node: Option<&NodeInfo> = None;
523            for file_nodes in nodes.values() {
524                if let Some(node) = file_nodes.get(decl_id) {
525                    target_node = Some(node);
526                    break;
527                }
528            }
529            let node = target_node?;
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            return Some((file_path, loc.offset, loc.length));
534        }
535    }
536
537    let mut refs = HashMap::new();
538
539    // Only consider nodes from the current file that have references
540    for (id, content) in current_file_nodes {
541        if content.referenced_declaration.is_none() {
542            continue;
543        }
544
545        let Some(src_loc) = SourceLoc::parse(&content.src) else {
546            continue;
547        };
548
549        if src_loc.offset <= position && position < src_loc.end() {
550            let diff = src_loc.length;
551            if !refs.contains_key(&diff) || refs[&diff] <= *id {
552                refs.insert(diff, *id);
553            }
554        }
555    }
556
557    if refs.is_empty() {
558        // Check if we're on the string part of an import statement
559        // ImportDirective nodes have absolutePath pointing to the imported file
560        let tmp = current_file_nodes.iter();
561        for (_id, content) in tmp {
562            if content.node_type == Some("ImportDirective".to_string()) {
563                let Some(src_loc) = SourceLoc::parse(&content.src) else {
564                    continue;
565                };
566
567                if src_loc.offset <= position
568                    && position < src_loc.end()
569                    && let Some(import_path) = &content.absolute_path
570                {
571                    return Some((import_path.clone(), 0, 0));
572                }
573            }
574        }
575        return None;
576    }
577
578    // Find the reference with minimum diff (most specific)
579    let min_diff = *refs.keys().min()?;
580    let chosen_id = refs[&min_diff];
581    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
582
583    // Search for the referenced declaration across all files
584    let mut target_node: Option<&NodeInfo> = None;
585    for file_nodes in nodes.values() {
586        if let Some(node) = file_nodes.get(&ref_id) {
587            target_node = Some(node);
588            break;
589        }
590    }
591
592    let node = target_node?;
593
594    // Get location from nameLocation or src
595    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
596    let loc = SourceLoc::parse(loc_str)?;
597    let file_path = id_to_path.get(&loc.file_id_str())?.clone();
598
599    Some((file_path, loc.offset, loc.length))
600}
601
602/// Go-to-declaration using pre-built `CachedBuild` indices.
603/// Avoids redundant O(N) AST traversal by reusing cached node maps.
604pub fn goto_declaration_cached(
605    build: &CachedBuild,
606    file_uri: &Url,
607    position: Position,
608    source_bytes: &[u8],
609) -> Option<Location> {
610    let byte_position = pos_to_bytes(source_bytes, position);
611
612    if let Some((file_path, location_bytes, length)) = goto_bytes(
613        &build.nodes,
614        &build.path_to_abs,
615        &build.id_to_path_map,
616        &build.external_refs,
617        file_uri.as_ref(),
618        byte_position,
619    ) {
620        let target_file_path = std::path::Path::new(&file_path);
621        let absolute_path = if target_file_path.is_absolute() {
622            target_file_path.to_path_buf()
623        } else {
624            // Resolve relative paths against the current file's directory,
625            // not CWD. This handles solc standard-json output where
626            // absolutePath is relative (e.g. "A.sol") and the server's CWD
627            // differs from the project root.
628            let base = file_uri
629                .to_file_path()
630                .ok()
631                .and_then(|p| p.parent().map(|d| d.to_path_buf()))
632                .or_else(|| std::env::current_dir().ok())
633                .unwrap_or_default();
634            base.join(target_file_path)
635        };
636
637        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
638            && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
639            && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
640            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
641        {
642            return Some(Location {
643                uri: target_uri,
644                range: Range {
645                    start: start_pos,
646                    end: end_pos,
647                },
648            });
649        }
650    };
651
652    None
653}
654
655/// Name-based AST goto — resolves by searching cached AST nodes for identifiers
656/// matching `name` in the current file, then following `referencedDeclaration`.
657///
658/// Unlike `goto_declaration_cached` which matches by byte offset (breaks on dirty files),
659/// this reads the identifier text from the **built source** (on disk) at each node's
660/// `src` range and compares it to the cursor name. Works on dirty files because the
661/// AST node relationships (referencedDeclaration) are still valid — only the byte
662/// offsets in the current buffer are stale.
663/// `byte_hint` is the cursor's byte offset in the dirty buffer, used to pick
664/// the closest matching node when multiple nodes share the same name (overloads).
665pub fn goto_declaration_by_name(
666    cached_build: &CachedBuild,
667    file_uri: &Url,
668    name: &str,
669    byte_hint: usize,
670) -> Option<Location> {
671    let path = match file_uri.as_ref().starts_with("file://") {
672        true => &file_uri.as_ref()[7..],
673        false => file_uri.as_ref(),
674    };
675    let abs_path = cached_build.path_to_abs.get(path)?;
676    // Read the built source from disk to extract identifier text at src ranges
677    let built_source = std::fs::read_to_string(abs_path).ok()?;
678
679    // Collect all matching nodes: (distance_to_hint, span_size, ref_id)
680    let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
681
682    let tmp = {
683        let this = cached_build.nodes.get(abs_path)?;
684        this.iter()
685    };
686    for (_id, node) in tmp {
687        let ref_id = match node.referenced_declaration {
688            Some(id) => id,
689            None => continue,
690        };
691
692        // Parse the node's src to get the byte range in the built source
693        let Some(src_loc) = SourceLoc::parse(&node.src) else {
694            continue;
695        };
696        let start = src_loc.offset;
697        let length = src_loc.length;
698
699        if start + length > built_source.len() {
700            continue;
701        }
702
703        let node_text = &built_source[start..start + length];
704
705        // Check if this node's text matches the name we're looking for.
706        // For simple identifiers, the text equals the name directly.
707        // For member access (e.g. `x.toInt128()`), check if the text contains
708        // `.name(` or ends with `.name`.
709        let matches = node_text == name
710            || node_text.contains(&format!(".{name}("))
711            || node_text.ends_with(&format!(".{name}"));
712
713        if matches {
714            // Distance from the byte_hint (cursor in dirty buffer) to the
715            // node's src range. The closest node is most likely the one the
716            // cursor is on, even if byte offsets shifted slightly.
717            let distance = if byte_hint >= start && byte_hint < start + length {
718                0 // cursor is inside this node's range
719            } else if byte_hint < start {
720                start - byte_hint
721            } else {
722                byte_hint - (start + length)
723            };
724            candidates.push((distance, length, ref_id));
725        }
726    }
727
728    // Sort by distance (closest to cursor hint), then by span size (narrowest)
729    candidates.sort_by_key(|&(dist, span, _)| (dist, span));
730    let ref_id = candidates.first()?.2;
731
732    // Find the declaration node across all files
733    let mut target_node: Option<&NodeInfo> = None;
734    for file_nodes in cached_build.nodes.values() {
735        if let Some(node) = file_nodes.get(&ref_id) {
736            target_node = Some(node);
737            break;
738        }
739    }
740
741    let node = target_node?;
742
743    // Parse the target's nameLocation or src
744    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
745    let loc = SourceLoc::parse(loc_str)?;
746
747    let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
748    let location_bytes = loc.offset;
749    let length = loc.length;
750
751    let target_file_path = std::path::Path::new(file_path);
752    let absolute_path = if target_file_path.is_absolute() {
753        target_file_path.to_path_buf()
754    } else {
755        let base = file_uri
756            .to_file_path()
757            .ok()
758            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
759            .or_else(|| std::env::current_dir().ok())
760            .unwrap_or_default();
761        base.join(target_file_path)
762    };
763
764    let target_source_bytes = std::fs::read(&absolute_path).ok()?;
765    let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
766    let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
767    let target_uri = Url::from_file_path(&absolute_path).ok()?;
768
769    Some(Location {
770        uri: target_uri,
771        range: Range {
772            start: start_pos,
773            end: end_pos,
774        },
775    })
776}
777
778// ── Tree-sitter enhanced goto ──────────────────────────────────────────────
779
780/// Context extracted from the cursor position via tree-sitter.
781#[derive(Debug, Clone)]
782pub struct CursorContext {
783    /// The identifier text under the cursor.
784    pub name: String,
785    /// Enclosing function name (if any).
786    pub function: Option<String>,
787    /// Enclosing contract/interface/library name (if any).
788    pub contract: Option<String>,
789    /// Object in a member access expression (e.g. `SqrtPriceMath` in
790    /// `SqrtPriceMath.getAmount0Delta`). Set when the cursor is on the
791    /// property side of a dot expression.
792    pub object: Option<String>,
793    /// Number of arguments at the call site (for overload disambiguation).
794    /// Set when the cursor is on a function name inside a `call_expression`.
795    pub arg_count: Option<usize>,
796    /// Inferred argument types at the call site (e.g. `["uint160", "uint160", "int128"]`).
797    /// `None` entries mean the type couldn't be inferred for that argument.
798    pub arg_types: Vec<Option<String>>,
799}
800
801/// Parse Solidity source with tree-sitter.
802fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
803    let mut parser = Parser::new();
804    parser
805        .set_language(&tree_sitter_solidity::LANGUAGE.into())
806        .expect("failed to load Solidity grammar");
807    parser.parse(source, None)
808}
809
810/// Validate that the text at a goto target location matches the expected name.
811///
812/// Used to reject tree-sitter results that land on the wrong identifier.
813/// AST results are NOT validated because the AST can legitimately resolve
814/// to a different name (e.g. `.selector` → error declaration).
815pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
816    let line = location.range.start.line as usize;
817    let start_col = location.range.start.character as usize;
818    let end_col = location.range.end.character as usize;
819
820    if let Some(line_text) = target_source.lines().nth(line)
821        && end_col <= line_text.len()
822    {
823        return &line_text[start_col..end_col] == expected_name;
824    }
825    // Can't read target — assume valid
826    true
827}
828
829/// Find the deepest named node at the given byte offset.
830fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
831    if byte < node.start_byte() || byte >= node.end_byte() {
832        return None;
833    }
834    let mut cursor = node.walk();
835    for child in node.children(&mut cursor) {
836        if child.start_byte() <= byte
837            && byte < child.end_byte()
838            && let Some(deeper) = ts_node_at_byte(child, byte)
839        {
840            return Some(deeper);
841        }
842    }
843    Some(node)
844}
845
846/// Get the identifier name from a node (first `identifier` child or the node itself).
847fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
848    let mut cursor = node.walk();
849    node.children(&mut cursor)
850        .find(|c| c.kind() == "identifier" && c.is_named())
851        .map(|c| &source[c.byte_range()])
852}
853
854/// Infer the type of an expression node using tree-sitter.
855///
856/// For identifiers, walks up to find the variable declaration and extracts its type.
857/// For literals, infers the type from the literal kind.
858/// For function calls, returns None (would need return type resolution).
859fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
860    // Unwrap call_argument → get inner expression
861    let expr = if arg_node.kind() == "call_argument" {
862        let mut c = arg_node.walk();
863        arg_node.children(&mut c).find(|ch| ch.is_named())?
864    } else {
865        arg_node
866    };
867
868    match expr.kind() {
869        "identifier" => {
870            let var_name = &source[expr.byte_range()];
871            // Walk up scopes to find the variable declaration
872            find_variable_type(expr, source, var_name)
873        }
874        "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
875        "boolean_literal" => Some("bool".into()),
876        "string_literal" | "hex_string_literal" => Some("string".into()),
877        _ => None,
878    }
879}
880
881/// Find the type of a variable by searching upward through enclosing scopes.
882///
883/// Looks for `parameter`, `variable_declaration`, and `state_variable_declaration`
884/// nodes whose identifier matches the variable name.
885fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
886    let mut scope = from.parent();
887    while let Some(node) = scope {
888        match node.kind() {
889            "function_definition" | "modifier_definition" | "constructor_definition" => {
890                // Check parameters
891                let mut c = node.walk();
892                for child in node.children(&mut c) {
893                    if child.kind() == "parameter"
894                        && let Some(id) = ts_child_id_text(child, source)
895                        && id == var_name
896                    {
897                        // Extract the type from this parameter
898                        let mut pc = child.walk();
899                        return child
900                            .children(&mut pc)
901                            .find(|c| {
902                                matches!(
903                                    c.kind(),
904                                    "type_name"
905                                        | "primitive_type"
906                                        | "user_defined_type"
907                                        | "mapping"
908                                )
909                            })
910                            .map(|t| source[t.byte_range()].trim().to_string());
911                    }
912                }
913            }
914            "function_body" | "block_statement" | "unchecked_block" => {
915                // Check local variable declarations
916                let mut c = node.walk();
917                for child in node.children(&mut c) {
918                    if (child.kind() == "variable_declaration_statement"
919                        || child.kind() == "variable_declaration")
920                        && let Some(id) = ts_child_id_text(child, source)
921                        && id == var_name
922                    {
923                        let mut pc = child.walk();
924                        return child
925                            .children(&mut pc)
926                            .find(|c| {
927                                matches!(
928                                    c.kind(),
929                                    "type_name"
930                                        | "primitive_type"
931                                        | "user_defined_type"
932                                        | "mapping"
933                                )
934                            })
935                            .map(|t| source[t.byte_range()].trim().to_string());
936                    }
937                }
938            }
939            "contract_declaration" | "library_declaration" | "interface_declaration" => {
940                // Check state variables
941                if let Some(body) = ts_find_child(node, "contract_body") {
942                    let mut c = body.walk();
943                    for child in body.children(&mut c) {
944                        if child.kind() == "state_variable_declaration"
945                            && let Some(id) = ts_child_id_text(child, source)
946                            && id == var_name
947                        {
948                            let mut pc = child.walk();
949                            return child
950                                .children(&mut pc)
951                                .find(|c| {
952                                    matches!(
953                                        c.kind(),
954                                        "type_name"
955                                            | "primitive_type"
956                                            | "user_defined_type"
957                                            | "mapping"
958                                    )
959                                })
960                                .map(|t| source[t.byte_range()].trim().to_string());
961                        }
962                    }
963                }
964            }
965            _ => {}
966        }
967        scope = node.parent();
968    }
969    None
970}
971
972/// Infer argument types at a call site by examining each `call_argument` child.
973fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
974    let mut cursor = call_node.walk();
975    call_node
976        .children(&mut cursor)
977        .filter(|c| c.kind() == "call_argument")
978        .map(|arg| infer_argument_type(arg, source))
979        .collect()
980}
981
982/// Pick the best overload from multiple declarations based on argument types.
983///
984/// Strategy:
985/// 1. If only one declaration, return it.
986/// 2. Filter by argument count first.
987/// 3. Among count-matched declarations, score by how many argument types match.
988/// 4. Return the highest-scoring declaration.
989fn best_overload<'a>(
990    decls: &'a [TsDeclaration],
991    arg_count: Option<usize>,
992    arg_types: &[Option<String>],
993) -> Option<&'a TsDeclaration> {
994    if decls.len() == 1 {
995        return decls.first();
996    }
997    if decls.is_empty() {
998        return None;
999    }
1000
1001    // Filter to only function declarations (skip parameters, variables, etc.)
1002    let func_decls: Vec<&TsDeclaration> =
1003        decls.iter().filter(|d| d.param_count.is_some()).collect();
1004
1005    if func_decls.is_empty() {
1006        return decls.first();
1007    }
1008
1009    // If we have arg_count, filter by it
1010    let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1011        let matched: Vec<_> = func_decls
1012            .iter()
1013            .filter(|d| d.param_count == Some(ac))
1014            .collect();
1015        if matched.len() == 1 {
1016            return Some(matched[0]);
1017        }
1018        if matched.is_empty() {
1019            // No count match — fall back to all
1020            func_decls.iter().collect()
1021        } else {
1022            matched
1023        }
1024    } else {
1025        func_decls.iter().collect()
1026    };
1027
1028    // Score each candidate by how many argument types match parameter types
1029    if !arg_types.is_empty() {
1030        let mut best: Option<(&TsDeclaration, usize)> = None;
1031        for &&decl in &count_matched {
1032            let score = arg_types
1033                .iter()
1034                .zip(decl.param_types.iter())
1035                .filter(|(arg_ty, param_ty)| {
1036                    if let Some(at) = arg_ty {
1037                        at == param_ty.as_str()
1038                    } else {
1039                        false
1040                    }
1041                })
1042                .count();
1043            if best.is_none() || score > best.unwrap().1 {
1044                best = Some((decl, score));
1045            }
1046        }
1047        if let Some((decl, _)) = best {
1048            return Some(decl);
1049        }
1050    }
1051
1052    // Fallback: return first count-matched or first overall
1053    count_matched.first().map(|d| **d).or(decls.first())
1054}
1055
1056/// Extract cursor context: the identifier under the cursor and its ancestor names.
1057///
1058/// Walks up the tree-sitter parse tree to find the enclosing function and contract.
1059pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1060    let tree = ts_parse(source)?;
1061    let byte = pos_to_bytes(source.as_bytes(), position);
1062    let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1063
1064    // The leaf should be an identifier (or we find the nearest identifier)
1065    let id_node = if leaf.kind() == "identifier" {
1066        leaf
1067    } else {
1068        // Check parent — cursor might be just inside a node that contains an identifier
1069        let parent = leaf.parent()?;
1070        if parent.kind() == "identifier" {
1071            parent
1072        } else {
1073            return None;
1074        }
1075    };
1076
1077    let name = source[id_node.byte_range()].to_string();
1078    let mut function = None;
1079    let mut contract = None;
1080
1081    // Detect member access: if the identifier is the `property` side of a
1082    // member_expression (e.g. `SqrtPriceMath.getAmount0Delta`), extract
1083    // the object name so the caller can resolve cross-file.
1084    let object = id_node.parent().and_then(|parent| {
1085        if parent.kind() == "member_expression" {
1086            let prop = parent.child_by_field_name("property")?;
1087            // Only set object when cursor is on the property, not the object side
1088            if prop.id() == id_node.id() {
1089                let obj = parent.child_by_field_name("object")?;
1090                Some(source[obj.byte_range()].to_string())
1091            } else {
1092                None
1093            }
1094        } else {
1095            None
1096        }
1097    });
1098
1099    // Count arguments and infer types at the call site for overload disambiguation.
1100    // Walk up from the identifier to find an enclosing `call_expression`,
1101    // then count its `call_argument` children and infer their types.
1102    let (arg_count, arg_types) = {
1103        let mut node = id_node.parent();
1104        let mut result = (None, vec![]);
1105        while let Some(n) = node {
1106            if n.kind() == "call_expression" {
1107                let types = infer_call_arg_types(n, source);
1108                result = (Some(types.len()), types);
1109                break;
1110            }
1111            node = n.parent();
1112        }
1113        result
1114    };
1115
1116    // Walk ancestors
1117    let mut current = id_node.parent();
1118    while let Some(node) = current {
1119        match node.kind() {
1120            "function_definition" | "modifier_definition" if function.is_none() => {
1121                function = ts_child_id_text(node, source).map(String::from);
1122            }
1123            "constructor_definition" if function.is_none() => {
1124                function = Some("constructor".into());
1125            }
1126            "contract_declaration" | "interface_declaration" | "library_declaration"
1127                if contract.is_none() =>
1128            {
1129                contract = ts_child_id_text(node, source).map(String::from);
1130            }
1131            _ => {}
1132        }
1133        current = node.parent();
1134    }
1135
1136    Some(CursorContext {
1137        name,
1138        function,
1139        contract,
1140        object,
1141        arg_count,
1142        arg_types,
1143    })
1144}
1145
1146/// Information about a declaration found by tree-sitter.
1147#[derive(Debug, Clone)]
1148pub struct TsDeclaration {
1149    /// Position range of the declaration identifier.
1150    pub range: Range,
1151    /// What kind of declaration (contract, function, state_variable, etc.).
1152    pub kind: &'static str,
1153    /// Container name (contract/struct that owns this declaration).
1154    pub container: Option<String>,
1155    /// Number of parameters (for function/modifier declarations).
1156    pub param_count: Option<usize>,
1157    /// Parameter type signature (e.g. `["uint160", "uint160", "int128"]`).
1158    /// Used for overload disambiguation.
1159    pub param_types: Vec<String>,
1160}
1161
1162/// Find all declarations of a name in a source file using tree-sitter.
1163///
1164/// Scans the parse tree for declaration nodes (state variables, functions, events,
1165/// errors, structs, enums, contracts, etc.) whose identifier matches `name`.
1166pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1167    let tree = match ts_parse(source) {
1168        Some(t) => t,
1169        None => return vec![],
1170    };
1171    let mut results = Vec::new();
1172    collect_declarations(tree.root_node(), source, name, None, &mut results);
1173    results
1174}
1175
1176fn collect_declarations(
1177    node: Node,
1178    source: &str,
1179    name: &str,
1180    container: Option<&str>,
1181    out: &mut Vec<TsDeclaration>,
1182) {
1183    let mut cursor = node.walk();
1184    for child in node.children(&mut cursor) {
1185        if !child.is_named() {
1186            continue;
1187        }
1188        match child.kind() {
1189            "contract_declaration" | "interface_declaration" | "library_declaration" => {
1190                if let Some(id_name) = ts_child_id_text(child, source) {
1191                    if id_name == name {
1192                        out.push(TsDeclaration {
1193                            range: id_range(child),
1194                            kind: child.kind(),
1195                            container: container.map(String::from),
1196                            param_count: None,
1197                            param_types: vec![],
1198                        });
1199                    }
1200                    // Recurse into contract body
1201                    if let Some(body) = ts_find_child(child, "contract_body") {
1202                        collect_declarations(body, source, name, Some(id_name), out);
1203                    }
1204                }
1205            }
1206            "function_definition" | "modifier_definition" => {
1207                if let Some(id_name) = ts_child_id_text(child, source) {
1208                    if id_name == name {
1209                        let types = parameter_type_signature(child, source);
1210                        out.push(TsDeclaration {
1211                            range: id_range(child),
1212                            kind: child.kind(),
1213                            container: container.map(String::from),
1214                            param_count: Some(types.len()),
1215                            param_types: types.into_iter().map(String::from).collect(),
1216                        });
1217                    }
1218                    // Check function parameters
1219                    collect_parameters(child, source, name, container, out);
1220                    // Recurse into function body for local variables
1221                    if let Some(body) = ts_find_child(child, "function_body") {
1222                        collect_declarations(body, source, name, container, out);
1223                    }
1224                }
1225            }
1226            "constructor_definition" => {
1227                if name == "constructor" {
1228                    let types = parameter_type_signature(child, source);
1229                    out.push(TsDeclaration {
1230                        range: ts_range(child),
1231                        kind: "constructor_definition",
1232                        container: container.map(String::from),
1233                        param_count: Some(types.len()),
1234                        param_types: types.into_iter().map(String::from).collect(),
1235                    });
1236                }
1237                // Check constructor parameters
1238                collect_parameters(child, source, name, container, out);
1239                if let Some(body) = ts_find_child(child, "function_body") {
1240                    collect_declarations(body, source, name, container, out);
1241                }
1242            }
1243            "state_variable_declaration" | "variable_declaration" => {
1244                if let Some(id_name) = ts_child_id_text(child, source)
1245                    && id_name == name
1246                {
1247                    out.push(TsDeclaration {
1248                        range: id_range(child),
1249                        kind: child.kind(),
1250                        container: container.map(String::from),
1251                        param_count: None,
1252                        param_types: vec![],
1253                    });
1254                }
1255            }
1256            "struct_declaration" => {
1257                if let Some(id_name) = ts_child_id_text(child, source) {
1258                    if id_name == name {
1259                        out.push(TsDeclaration {
1260                            range: id_range(child),
1261                            kind: "struct_declaration",
1262                            container: container.map(String::from),
1263                            param_count: None,
1264                            param_types: vec![],
1265                        });
1266                    }
1267                    if let Some(body) = ts_find_child(child, "struct_body") {
1268                        collect_declarations(body, source, name, Some(id_name), out);
1269                    }
1270                }
1271            }
1272            "enum_declaration" => {
1273                if let Some(id_name) = ts_child_id_text(child, source) {
1274                    if id_name == name {
1275                        out.push(TsDeclaration {
1276                            range: id_range(child),
1277                            kind: "enum_declaration",
1278                            container: container.map(String::from),
1279                            param_count: None,
1280                            param_types: vec![],
1281                        });
1282                    }
1283                    // Check enum values
1284                    if let Some(body) = ts_find_child(child, "enum_body") {
1285                        let mut ecur = body.walk();
1286                        for val in body.children(&mut ecur) {
1287                            if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1288                                out.push(TsDeclaration {
1289                                    range: ts_range(val),
1290                                    kind: "enum_value",
1291                                    container: Some(id_name.to_string()),
1292                                    param_count: None,
1293                                    param_types: vec![],
1294                                });
1295                            }
1296                        }
1297                    }
1298                }
1299            }
1300            "event_definition" | "error_declaration" => {
1301                if let Some(id_name) = ts_child_id_text(child, source)
1302                    && id_name == name
1303                {
1304                    out.push(TsDeclaration {
1305                        range: id_range(child),
1306                        kind: child.kind(),
1307                        container: container.map(String::from),
1308                        param_count: None,
1309                        param_types: vec![],
1310                    });
1311                }
1312            }
1313            "user_defined_type_definition" => {
1314                if let Some(id_name) = ts_child_id_text(child, source)
1315                    && id_name == name
1316                {
1317                    out.push(TsDeclaration {
1318                        range: id_range(child),
1319                        kind: "user_defined_type_definition",
1320                        container: container.map(String::from),
1321                        param_count: None,
1322                        param_types: vec![],
1323                    });
1324                }
1325            }
1326            // Recurse into blocks, if-else, loops, etc.
1327            _ => {
1328                collect_declarations(child, source, name, container, out);
1329            }
1330        }
1331    }
1332}
1333
1334/// Extract the type signature from a function's parameters.
1335///
1336/// Returns a list of type strings, e.g. `["uint160", "uint160", "int128"]`.
1337/// For complex types (mappings, arrays, user-defined), returns the full
1338/// text of the type node.
1339fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1340    let mut cursor = node.walk();
1341    node.children(&mut cursor)
1342        .filter(|c| c.kind() == "parameter")
1343        .filter_map(|param| {
1344            let mut pc = param.walk();
1345            param
1346                .children(&mut pc)
1347                .find(|c| {
1348                    matches!(
1349                        c.kind(),
1350                        "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1351                    )
1352                })
1353                .map(|t| source[t.byte_range()].trim())
1354        })
1355        .collect()
1356}
1357
1358/// Collect parameter declarations from a function/constructor node.
1359fn collect_parameters(
1360    node: Node,
1361    source: &str,
1362    name: &str,
1363    container: Option<&str>,
1364    out: &mut Vec<TsDeclaration>,
1365) {
1366    let mut cursor = node.walk();
1367    for child in node.children(&mut cursor) {
1368        if child.kind() == "parameter"
1369            && let Some(id_name) = ts_child_id_text(child, source)
1370            && id_name == name
1371        {
1372            out.push(TsDeclaration {
1373                range: id_range(child),
1374                kind: "parameter",
1375                container: container.map(String::from),
1376                param_count: None,
1377                param_types: vec![],
1378            });
1379        }
1380    }
1381}
1382
1383/// Tree-sitter range helper.
1384fn ts_range(node: Node) -> Range {
1385    let s = node.start_position();
1386    let e = node.end_position();
1387    Range {
1388        start: Position::new(s.row as u32, s.column as u32),
1389        end: Position::new(e.row as u32, e.column as u32),
1390    }
1391}
1392
1393/// Get the range of the identifier child within a declaration node.
1394fn id_range(node: Node) -> Range {
1395    let mut cursor = node.walk();
1396    node.children(&mut cursor)
1397        .find(|c| c.kind() == "identifier" && c.is_named())
1398        .map(|c| ts_range(c))
1399        .unwrap_or_else(|| ts_range(node))
1400}
1401
1402fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1403    let mut cursor = node.walk();
1404    node.children(&mut cursor).find(|c| c.kind() == kind)
1405}
1406
1407/// Tree-sitter enhanced goto definition.
1408///
1409/// Uses tree-sitter to find the identifier under the cursor and its scope,
1410/// then resolves via the CompletionCache (for cross-file/semantic resolution),
1411/// and finally uses tree-sitter to find the declaration position in the target file.
1412///
1413/// Falls back to None if resolution fails — caller should try the existing AST-based path.
1414pub fn goto_definition_ts(
1415    source: &str,
1416    position: Position,
1417    file_uri: &Url,
1418    completion_cache: &crate::completion::CompletionCache,
1419    text_cache: &HashMap<String, (i32, String)>,
1420) -> Option<Location> {
1421    let ctx = cursor_context(source, position)?;
1422
1423    // Member access: cursor is on `getAmount0Delta` in `SqrtPriceMath.getAmount0Delta`.
1424    // Look up the object (SqrtPriceMath) in the completion cache to find its file,
1425    // then search that file for the member declaration.
1426    // When multiple overloads exist, disambiguate by argument count and types.
1427    if let Some(obj_name) = &ctx.object {
1428        if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1429            let target_source = read_target_source(&path, text_cache)?;
1430            let target_uri = Url::from_file_path(&path).ok()?;
1431            let decls = find_declarations_by_name(&target_source, &ctx.name);
1432            if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1433                return Some(Location {
1434                    uri: target_uri,
1435                    range: d.range,
1436                });
1437            }
1438        }
1439        // Object might be in the same file (e.g. a struct or contract in this file)
1440        let decls = find_declarations_by_name(source, &ctx.name);
1441        if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1442            return Some(Location {
1443                uri: file_uri.clone(),
1444                range: d.range,
1445            });
1446        }
1447    }
1448
1449    // Step 1: Try to resolve via CompletionCache to find which file + name the declaration is in.
1450    // Use the scope chain by names: find the contract scope, then resolve the name.
1451    let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1452
1453    match resolved {
1454        Some(ResolvedTarget::SameFile) => {
1455            // Declaration is in the same file — find it with tree-sitter
1456            find_best_declaration(source, &ctx, file_uri)
1457        }
1458        Some(ResolvedTarget::OtherFile { path, name }) => {
1459            // Declaration is in another file — read target source and find by name
1460            let target_source = read_target_source(&path, text_cache);
1461            let target_source = target_source?;
1462            let target_uri = Url::from_file_path(&path).ok()?;
1463            let decls = find_declarations_by_name(&target_source, &name);
1464            decls.first().map(|d| Location {
1465                uri: target_uri,
1466                range: d.range,
1467            })
1468        }
1469        None => {
1470            // CompletionCache couldn't resolve — try same-file tree-sitter lookup as fallback
1471            find_best_declaration(source, &ctx, file_uri)
1472        }
1473    }
1474}
1475
1476#[derive(Debug)]
1477enum ResolvedTarget {
1478    /// Declaration is in the same file as the usage.
1479    SameFile,
1480    /// Declaration is in a different file.
1481    OtherFile { path: String, name: String },
1482}
1483
1484/// Try to resolve an identifier using the CompletionCache.
1485///
1486/// Finds the scope by matching ancestor names (contract, function) against
1487/// the cache's scope data, then resolves the name to a type and traces
1488/// back to the declaring file.
1489fn resolve_via_cache(
1490    ctx: &CursorContext,
1491    file_uri: &Url,
1492    cache: &crate::completion::CompletionCache,
1493) -> Option<ResolvedTarget> {
1494    // Find the contract scope node_id by name
1495    let contract_scope = ctx
1496        .contract
1497        .as_ref()
1498        .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1499        .copied();
1500
1501    // Try scope-based resolution: look in the contract's scope_declarations
1502    if let Some(contract_id) = contract_scope {
1503        // Check function scope if we're inside one
1504        if let Some(func_name) = &ctx.function {
1505            // Find the function scope: look for a scope whose parent is this contract
1506            // and which has a declaration for this function name
1507            if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1508                // Check declarations in this function scope first
1509                if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1510                    && decls.iter().any(|d| d.name == ctx.name)
1511                {
1512                    return Some(ResolvedTarget::SameFile);
1513                }
1514            }
1515        }
1516
1517        // Check contract scope declarations (state variables, functions)
1518        if let Some(decls) = cache.scope_declarations.get(&contract_id)
1519            && decls.iter().any(|d| d.name == ctx.name)
1520        {
1521            return Some(ResolvedTarget::SameFile);
1522        }
1523
1524        // Check inherited contracts (C3 linearization)
1525        if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1526            for &base_id in bases.iter().skip(1) {
1527                if let Some(decls) = cache.scope_declarations.get(&base_id)
1528                    && decls.iter().any(|d| d.name == ctx.name)
1529                {
1530                    // Found in a base contract — find which file it's in
1531                    // Reverse lookup: base_id → contract name → file
1532                    let base_name = cache
1533                        .name_to_node_id
1534                        .iter()
1535                        .find(|&(_, &id)| id == base_id)
1536                        .map(|(name, _)| name.clone());
1537
1538                    if let Some(base_name) = base_name
1539                        && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1540                    {
1541                        return Some(ResolvedTarget::OtherFile {
1542                            path,
1543                            name: ctx.name.clone(),
1544                        });
1545                    }
1546                    // Base contract might be in the same file
1547                    return Some(ResolvedTarget::SameFile);
1548                }
1549            }
1550        }
1551    }
1552
1553    // Check if the name is a contract/library/interface name
1554    if cache.name_to_node_id.contains_key(&ctx.name) {
1555        // Could be same file or different file — check if it's in the current file
1556        if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1557            let current_path = file_uri.to_file_path().ok()?;
1558            let current_str = current_path.to_str()?;
1559            if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1560                return Some(ResolvedTarget::SameFile);
1561            }
1562            return Some(ResolvedTarget::OtherFile {
1563                path,
1564                name: ctx.name.clone(),
1565            });
1566        }
1567        return Some(ResolvedTarget::SameFile);
1568    }
1569
1570    // Flat fallback — name_to_type knows about it but we can't determine the file
1571    if cache.name_to_type.contains_key(&ctx.name) {
1572        return Some(ResolvedTarget::SameFile);
1573    }
1574
1575    None
1576}
1577
1578/// Find the scope node_id for a function within a contract.
1579fn find_function_scope(
1580    cache: &crate::completion::CompletionCache,
1581    contract_id: NodeId,
1582    func_name: &str,
1583) -> Option<NodeId> {
1584    // Look for a scope whose parent is the contract and which is a function scope.
1585    // The function name should appear as a declaration in the contract scope,
1586    // and the function's own scope is the one whose parent is the contract.
1587    for (&scope_id, &parent_id) in &cache.scope_parent {
1588        if parent_id == contract_id {
1589            // This scope's parent is our contract — it might be a function scope.
1590            // Check if this scope has declarations (functions/blocks do).
1591            // We also check if the contract declares a function with this name.
1592            if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1593                && contract_decls.iter().any(|d| d.name == func_name)
1594            {
1595                // Found a child scope of the contract — could be the function.
1596                // Check if this scope_id has child scopes or declarations
1597                // that match what we'd expect for a function body.
1598                if cache.scope_declarations.contains_key(&scope_id)
1599                    || cache.scope_parent.values().any(|&p| p == scope_id)
1600                {
1601                    return Some(scope_id);
1602                }
1603            }
1604        }
1605    }
1606    None
1607}
1608
1609/// Find the file path for a contract by searching the CompletionCache's path_to_file_id.
1610fn find_file_for_contract(
1611    cache: &crate::completion::CompletionCache,
1612    contract_name: &str,
1613    _file_uri: &Url,
1614) -> Option<String> {
1615    // The completion cache doesn't directly map contract → file.
1616    // But scope_ranges + path_to_file_id can help.
1617    // For now, check if the contract's node_id appears in any scope_range,
1618    // then map file_id back to path.
1619    let node_id = cache.name_to_node_id.get(contract_name)?;
1620    let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1621    let file_id = scope_range.file_id;
1622
1623    // Reverse lookup: file_id → path
1624    cache
1625        .path_to_file_id
1626        .iter()
1627        .find(|&(_, &fid)| fid == file_id)
1628        .map(|(path, _)| path.clone())
1629}
1630
1631/// Read source for a target file — prefer text_cache (open buffers), fallback to disk.
1632fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1633    // Try text_cache by URI
1634    let uri = Url::from_file_path(path).ok()?;
1635    if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1636        return Some(content.clone());
1637    }
1638    // Fallback to disk
1639    std::fs::read_to_string(path).ok()
1640}
1641
1642/// Find the best matching declaration in the same file.
1643fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1644    let decls = find_declarations_by_name(source, &ctx.name);
1645    if decls.is_empty() {
1646        return None;
1647    }
1648
1649    // If there's only one declaration, use it
1650    if decls.len() == 1 {
1651        return Some(Location {
1652            uri: file_uri.clone(),
1653            range: decls[0].range,
1654        });
1655    }
1656
1657    // Multiple declarations — prefer the one in the same contract
1658    if let Some(contract_name) = &ctx.contract
1659        && let Some(d) = decls
1660            .iter()
1661            .find(|d| d.container.as_deref() == Some(contract_name))
1662    {
1663        return Some(Location {
1664            uri: file_uri.clone(),
1665            range: d.range,
1666        });
1667    }
1668
1669    // Fallback: return first declaration
1670    Some(Location {
1671        uri: file_uri.clone(),
1672        range: decls[0].range,
1673    })
1674}
1675
1676// ─────────────────────────────────────────────────────────────────────────────
1677// Code-action helpers (used by lsp.rs `code_action` handler)
1678// ─────────────────────────────────────────────────────────────────────────────
1679
1680/// What kind of edit a code action should produce.
1681#[derive(Debug, Clone, Copy)]
1682pub(crate) enum CodeActionKind<'a> {
1683    /// Insert fixed text at the very start of the file (line 0, col 0).
1684    InsertAtFileStart { text: &'a str },
1685
1686    /// Replace the token at `diag_range.start` with `replacement`.
1687    /// Used for deprecated-builtin fixes (now→block.timestamp, sha3→keccak256, …).
1688    /// When `walk_to` is `Some`, walk up to that ancestor node and replace its
1689    /// full span instead of just the leaf (e.g. `member_expression` for msg.gas).
1690    ReplaceToken {
1691        replacement: &'a str,
1692        walk_to: Option<&'a str>,
1693    },
1694
1695    /// Delete the token whose start byte falls inside `diag_range`
1696    /// (+ one trailing space when present).
1697    DeleteToken,
1698
1699    /// Delete the entire `variable_declaration_statement` containing the
1700    /// identifier at `diag_range.start`, including leading whitespace/newline.
1701    DeleteLocalVar,
1702
1703    /// Walk up the TS tree to the first ancestor whose kind matches `node_kind`,
1704    /// then delete that whole node including its preceding newline+indentation.
1705    /// Used for any "delete this whole statement/declaration" fix (e.g. unused import).
1706    DeleteNodeByKind { node_kind: &'a str },
1707
1708    /// Walk the TS tree up to `walk_to`, then delete the first child whose
1709    /// kind matches any entry in `child_kinds` (tried in order).
1710    DeleteChildNode {
1711        walk_to: &'a str,
1712        child_kinds: &'a [&'a str],
1713    },
1714
1715    /// Walk the TS tree up to `walk_to`, then replace the first child whose
1716    /// kind matches `child_kind` with `replacement`.
1717    ReplaceChildNode {
1718        walk_to: &'a str,
1719        child_kind: &'a str,
1720        replacement: &'a str,
1721    },
1722
1723    /// Walk the TS tree up to `walk_to`, then insert `text` immediately before
1724    /// the first child whose kind matches any entry in `before_child`.
1725    /// Used for 5424 (insert `virtual` before `returns`/`;`).
1726    InsertBeforeNode {
1727        walk_to: &'a str,
1728        before_child: &'a [&'a str],
1729        text: &'a str,
1730    },
1731}
1732
1733/// Compute the `TextEdit` for a code action using tree-sitter for precision.
1734///
1735/// Returns `None` when the tree cannot be parsed or the target node cannot be
1736/// located (caller should fall back to returning no action for that diagnostic).
1737pub(crate) fn code_action_edit(
1738    source: &str,
1739    diag_range: Range,
1740    kind: CodeActionKind<'_>,
1741) -> Option<TextEdit> {
1742    let source_bytes = source.as_bytes();
1743
1744    match kind {
1745        // ── Insert fixed text at the top of the file ──────────────────────────
1746        CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
1747            range: Range {
1748                start: Position {
1749                    line: 0,
1750                    character: 0,
1751                },
1752                end: Position {
1753                    line: 0,
1754                    character: 0,
1755                },
1756            },
1757            new_text: text.to_string(),
1758        }),
1759
1760        // ── Replace the token at diag_range.start ─────────────────────────────
1761        CodeActionKind::ReplaceToken {
1762            replacement,
1763            walk_to,
1764        } => {
1765            let tree = ts_parse(source)?;
1766            let byte = pos_to_bytes(source_bytes, diag_range.start);
1767            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1768            // When a walk_to node kind is specified, walk up to that ancestor
1769            // so we replace its full span (e.g. `member_expression` for
1770            // `msg.gas` or `block.blockhash`).
1771            if let Some(target_kind) = walk_to {
1772                loop {
1773                    if node.kind() == target_kind {
1774                        break;
1775                    }
1776                    node = node.parent()?;
1777                }
1778            }
1779            let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
1780            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1781            Some(TextEdit {
1782                range: Range {
1783                    start: start_pos,
1784                    end: end_pos,
1785                },
1786                new_text: replacement.to_string(),
1787            })
1788        }
1789
1790        // ── Delete the token at diag_range.start (+ optional trailing space) ──
1791        CodeActionKind::DeleteToken => {
1792            let tree = ts_parse(source)?;
1793            let byte = pos_to_bytes(source_bytes, diag_range.start);
1794            let node = ts_node_at_byte(tree.root_node(), byte)?;
1795            let start = node.start_byte();
1796            let end =
1797                if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
1798                    node.end_byte() + 1
1799                } else {
1800                    node.end_byte()
1801                };
1802            let start_pos = bytes_to_pos(source_bytes, start)?;
1803            let end_pos = bytes_to_pos(source_bytes, end)?;
1804            Some(TextEdit {
1805                range: Range {
1806                    start: start_pos,
1807                    end: end_pos,
1808                },
1809                new_text: String::new(),
1810            })
1811        }
1812
1813        // ── Delete the enclosing variable_declaration_statement ───────────────
1814        CodeActionKind::DeleteLocalVar => {
1815            let tree = ts_parse(source)?;
1816            let byte = pos_to_bytes(source_bytes, diag_range.start);
1817            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1818
1819            loop {
1820                if node.kind() == "variable_declaration_statement" {
1821                    break;
1822                }
1823                node = node.parent()?;
1824            }
1825
1826            // Consume the preceding newline + indentation so no blank line remains.
1827            let stmt_start = node.start_byte();
1828            let delete_from = if stmt_start > 0 {
1829                let mut i = stmt_start - 1;
1830                while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
1831                    i -= 1;
1832                }
1833                if source_bytes[i] == b'\n' {
1834                    i
1835                } else {
1836                    stmt_start
1837                }
1838            } else {
1839                stmt_start
1840            };
1841
1842            let start_pos = bytes_to_pos(source_bytes, delete_from)?;
1843            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1844            Some(TextEdit {
1845                range: Range {
1846                    start: start_pos,
1847                    end: end_pos,
1848                },
1849                new_text: String::new(),
1850            })
1851        }
1852
1853        // ── Walk up to `node_kind`, delete that whole node (+ preceding newline) ─
1854        CodeActionKind::DeleteNodeByKind { node_kind } => {
1855            let tree = ts_parse(source)?;
1856            let byte = pos_to_bytes(source_bytes, diag_range.start);
1857            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1858            loop {
1859                if node.kind() == node_kind {
1860                    break;
1861                }
1862                node = node.parent()?;
1863            }
1864            // Consume the preceding newline + indentation so no blank line remains.
1865            let node_start = node.start_byte();
1866            let delete_from = if node_start > 0 {
1867                let mut i = node_start - 1;
1868                while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
1869                    i -= 1;
1870                }
1871                if source_bytes[i] == b'\n' {
1872                    i
1873                } else {
1874                    node_start
1875                }
1876            } else {
1877                node_start
1878            };
1879            let start_pos = bytes_to_pos(source_bytes, delete_from)?;
1880            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1881            Some(TextEdit {
1882                range: Range {
1883                    start: start_pos,
1884                    end: end_pos,
1885                },
1886                new_text: String::new(),
1887            })
1888        }
1889
1890        // ── Walk up to `walk_to`, delete first child of `child_kind` ─────────
1891        //
1892        // Used for 4126 (free function visibility) and payable codes where the
1893        // diagnostic points to the whole function, not the bad token.
1894        CodeActionKind::DeleteChildNode {
1895            walk_to,
1896            child_kinds,
1897        } => {
1898            let tree = ts_parse(source)?;
1899            let byte = pos_to_bytes(source_bytes, diag_range.start);
1900            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1901            loop {
1902                if node.kind() == walk_to {
1903                    break;
1904                }
1905                node = node.parent()?;
1906            }
1907            let mut cursor = node.walk();
1908            let children: Vec<_> = node.children(&mut cursor).collect();
1909            let child = child_kinds
1910                .iter()
1911                .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
1912            let start = child.start_byte();
1913            let end = if child.end_byte() < source_bytes.len()
1914                && source_bytes[child.end_byte()] == b' '
1915            {
1916                child.end_byte() + 1
1917            } else {
1918                child.end_byte()
1919            };
1920            let start_pos = bytes_to_pos(source_bytes, start)?;
1921            let end_pos = bytes_to_pos(source_bytes, end)?;
1922            Some(TextEdit {
1923                range: Range {
1924                    start: start_pos,
1925                    end: end_pos,
1926                },
1927                new_text: String::new(),
1928            })
1929        }
1930
1931        // ── Walk up to `walk_to`, replace first child of `child_kind` ─────────
1932        //
1933        // Used for 1560/1159/4095: replace wrong visibility with `external`.
1934        CodeActionKind::ReplaceChildNode {
1935            walk_to,
1936            child_kind,
1937            replacement,
1938        } => {
1939            let tree = ts_parse(source)?;
1940            let byte = pos_to_bytes(source_bytes, diag_range.start);
1941            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1942            loop {
1943                if node.kind() == walk_to {
1944                    break;
1945                }
1946                node = node.parent()?;
1947            }
1948            let mut cursor = node.walk();
1949            let child = node
1950                .children(&mut cursor)
1951                .find(|c| c.kind() == child_kind)?;
1952            let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
1953            let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
1954            Some(TextEdit {
1955                range: Range {
1956                    start: start_pos,
1957                    end: end_pos,
1958                },
1959                new_text: replacement.to_string(),
1960            })
1961        }
1962
1963        // ── Walk up to `walk_to`, insert `text` before first matching child ───
1964        //
1965        // Used for 5424 (insert `virtual` before `return_type_definition` / `;`).
1966        //
1967        // `before_child` is tried in order — the first matching child kind wins.
1968        // This lets callers express "prefer returns, fall back to semicolon".
1969        CodeActionKind::InsertBeforeNode {
1970            walk_to,
1971            before_child,
1972            text,
1973        } => {
1974            let tree = ts_parse(source)?;
1975            let byte = pos_to_bytes(source_bytes, diag_range.start);
1976            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1977
1978            loop {
1979                if node.kind() == walk_to {
1980                    break;
1981                }
1982                node = node.parent()?;
1983            }
1984
1985            let mut cursor = node.walk();
1986            let children: Vec<_> = node.children(&mut cursor).collect();
1987
1988            // Try each `before_child` kind in order.
1989            for target_kind in before_child {
1990                if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
1991                    let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
1992                    return Some(TextEdit {
1993                        range: Range {
1994                            start: insert_pos,
1995                            end: insert_pos,
1996                        },
1997                        new_text: text.to_string(),
1998                    });
1999                }
2000            }
2001            None
2002        }
2003    }
2004}
2005
2006#[cfg(test)]
2007mod ts_tests {
2008    use super::*;
2009
2010    #[test]
2011    fn test_cursor_context_state_var() {
2012        let source = r#"
2013contract Token {
2014    uint256 public totalSupply;
2015    function mint(uint256 amount) public {
2016        totalSupply += amount;
2017    }
2018}
2019"#;
2020        // Cursor on `totalSupply` inside mint (line 4, col 8)
2021        let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2022        assert_eq!(ctx.name, "totalSupply");
2023        assert_eq!(ctx.function.as_deref(), Some("mint"));
2024        assert_eq!(ctx.contract.as_deref(), Some("Token"));
2025    }
2026
2027    #[test]
2028    fn test_cursor_context_top_level() {
2029        let source = r#"
2030contract Foo {}
2031contract Bar {}
2032"#;
2033        // Cursor on `Foo` (line 1, col 9) — the identifier of the contract declaration
2034        let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2035        assert_eq!(ctx.name, "Foo");
2036        assert!(ctx.function.is_none());
2037        // The identifier `Foo` is a child of contract_declaration, so contract is set
2038        assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2039    }
2040
2041    #[test]
2042    fn test_find_declarations() {
2043        let source = r#"
2044contract Token {
2045    uint256 public totalSupply;
2046    function mint(uint256 amount) public {
2047        totalSupply += amount;
2048    }
2049}
2050"#;
2051        let decls = find_declarations_by_name(source, "totalSupply");
2052        assert_eq!(decls.len(), 1);
2053        assert_eq!(decls[0].kind, "state_variable_declaration");
2054        assert_eq!(decls[0].container.as_deref(), Some("Token"));
2055    }
2056
2057    #[test]
2058    fn test_find_declarations_multiple_contracts() {
2059        let source = r#"
2060contract A {
2061    uint256 public value;
2062}
2063contract B {
2064    uint256 public value;
2065}
2066"#;
2067        let decls = find_declarations_by_name(source, "value");
2068        assert_eq!(decls.len(), 2);
2069        assert_eq!(decls[0].container.as_deref(), Some("A"));
2070        assert_eq!(decls[1].container.as_deref(), Some("B"));
2071    }
2072
2073    #[test]
2074    fn test_find_declarations_enum_value() {
2075        let source = "contract Foo { enum Status { Active, Paused } }";
2076        let decls = find_declarations_by_name(source, "Active");
2077        assert_eq!(decls.len(), 1);
2078        assert_eq!(decls[0].kind, "enum_value");
2079        assert_eq!(decls[0].container.as_deref(), Some("Status"));
2080    }
2081
2082    #[test]
2083    fn test_cursor_context_short_param() {
2084        let source = r#"
2085contract Shop {
2086    uint256 public TAX;
2087    constructor(uint256 price, uint16 tax, uint16 taxBase) {
2088        TAX = tax;
2089    }
2090}
2091"#;
2092        // Cursor on `tax` usage at line 4, col 14 (TAX = tax;)
2093        let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2094        assert_eq!(ctx.name, "tax");
2095        assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2096
2097        // Cursor on `TAX` at line 4, col 8
2098        let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2099        assert_eq!(ctx2.name, "TAX");
2100
2101        // Parameters are found as declarations
2102        let decls = find_declarations_by_name(source, "tax");
2103        assert_eq!(decls.len(), 1);
2104        assert_eq!(decls[0].kind, "parameter");
2105
2106        let decls_tax_base = find_declarations_by_name(source, "taxBase");
2107        assert_eq!(decls_tax_base.len(), 1);
2108        assert_eq!(decls_tax_base[0].kind, "parameter");
2109
2110        let decls_price = find_declarations_by_name(source, "price");
2111        assert_eq!(decls_price.len(), 1);
2112        assert_eq!(decls_price[0].kind, "parameter");
2113
2114        // State variable is also found
2115        let decls_tax_upper = find_declarations_by_name(source, "TAX");
2116        assert_eq!(decls_tax_upper.len(), 1);
2117        assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2118    }
2119
2120    #[test]
2121    fn test_delete_child_node_2462_constructor_public() {
2122        // ConstructorVisibility.sol: `constructor() public {}`
2123        // Diagnostic range: start={line:9, char:4}, end={line:11, char:5}
2124        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Warning 2462\n\ncontract ConstructorVisibility {\n    uint256 public value;\n\n    constructor() public {\n        value = 1;\n    }\n}\n";
2125        let diag_range = Range {
2126            start: Position {
2127                line: 8,
2128                character: 4,
2129            },
2130            end: Position {
2131                line: 10,
2132                character: 5,
2133            },
2134        };
2135        let source_bytes = source.as_bytes();
2136        let tree = ts_parse(source).expect("parse failed");
2137        let byte = pos_to_bytes(source_bytes, diag_range.start);
2138        eprintln!("2462 byte offset: {byte}");
2139        if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2140            loop {
2141                eprintln!(
2142                    "  ancestor: kind={} start={} end={}",
2143                    n.kind(),
2144                    n.start_byte(),
2145                    n.end_byte()
2146                );
2147                if n.kind() == "constructor_definition" {
2148                    let mut cursor = n.walk();
2149                    for child in n.children(&mut cursor) {
2150                        eprintln!(
2151                            "    child: kind={:?} text={:?}",
2152                            child.kind(),
2153                            &source[child.start_byte()..child.end_byte()]
2154                        );
2155                    }
2156                    break;
2157                }
2158                match n.parent() {
2159                    Some(p) => n = p,
2160                    None => break,
2161                }
2162            }
2163        }
2164        let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2165        let edit = code_action_edit(
2166            source,
2167            diag_range,
2168            CodeActionKind::DeleteChildNode {
2169                walk_to: "constructor_definition",
2170                child_kinds: &ck,
2171            },
2172        );
2173        assert!(edit.is_some(), "2462 fix returned None");
2174        let edit = edit.unwrap();
2175        assert_eq!(edit.new_text, "");
2176        let lines: Vec<&str> = source.lines().collect();
2177        let deleted = &lines[edit.range.start.line as usize]
2178            [edit.range.start.character as usize..edit.range.end.character as usize];
2179        assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2180    }
2181
2182    #[test]
2183    fn test_delete_child_node_9239_constructor_private() {
2184        // Fixture mirrors example/codeaction/ConstructorInvalidVisibility.sol
2185        // Diagnostic range from server: start={line:9, char:4}, end={line:11, char:5}
2186        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Error 9239: Constructor cannot have visibility.\n// Fix: remove the visibility specifier (private/external) from the constructor.\n\ncontract ConstructorPrivateVisibility {\n    uint256 public value;\n\n    constructor() private {\n        value = 1;\n    }\n}\n";
2187        let diag_range = Range {
2188            start: Position {
2189                line: 9,
2190                character: 4,
2191            },
2192            end: Position {
2193                line: 11,
2194                character: 5,
2195            },
2196        };
2197        let ck: Vec<&str> = vec!["modifier_invocation"];
2198        let edit = code_action_edit(
2199            source,
2200            diag_range,
2201            CodeActionKind::DeleteChildNode {
2202                walk_to: "constructor_definition",
2203                child_kinds: &ck,
2204            },
2205        );
2206        assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2207        let edit = edit.unwrap();
2208        // The edit should delete 'private ' — new_text must be empty
2209        assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2210        // The deleted text should contain 'private'
2211        let lines: Vec<&str> = source.lines().collect();
2212        let line_text = lines[edit.range.start.line as usize];
2213        let deleted =
2214            &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2215        assert!(
2216            deleted.contains("private"),
2217            "expected deleted text to contain 'private', got: {:?}",
2218            deleted
2219        );
2220    }
2221
2222    #[test]
2223    fn test_find_best_declaration_same_contract() {
2224        let source = r#"
2225contract A { uint256 public x; }
2226contract B { uint256 public x; }
2227"#;
2228        let ctx = CursorContext {
2229            name: "x".into(),
2230            function: None,
2231            contract: Some("B".into()),
2232            object: None,
2233            arg_count: None,
2234            arg_types: vec![],
2235        };
2236        let uri = Url::parse("file:///test.sol").unwrap();
2237        let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2238        // Should pick B's x (line 2), not A's x (line 1)
2239        assert_eq!(loc.range.start.line, 2);
2240    }
2241}
2242// temp