Skip to main content

solidity_language_server/
inlay_hints.rs

1use crate::goto::CachedBuild;
2use crate::types::{AbsPath, NodeId, SourceLoc};
3use serde_json::Value;
4use std::collections::HashMap;
5use tower_lsp::lsp_types::*;
6use tree_sitter::{Node, Parser};
7
8/// Parameter info resolved from the AST for a callable.
9#[derive(Debug, Clone)]
10struct ParamInfo {
11    /// Parameter names from the declaration.
12    names: Vec<String>,
13    /// Number of leading params to skip (1 for using-for library calls).
14    skip: usize,
15}
16
17/// Call-site info extracted from the AST, keyed by source byte offset.
18#[derive(Debug, Clone)]
19struct CallSite {
20    /// The resolved parameter info for this specific call.
21    info: ParamInfo,
22    /// Function/event name (for matching with tree-sitter).
23    name: String,
24    /// The AST node id of the called function/event declaration (for DocIndex lookup).
25    decl_id: NodeId,
26}
27
28/// Resolved callsite info returned to hover for param doc lookup.
29#[derive(Debug, Clone)]
30pub struct ResolvedCallSite {
31    /// The parameter name at the given argument index.
32    pub param_name: String,
33    /// The AST node id of the called function/event declaration.
34    pub decl_id: NodeId,
35}
36
37/// Both lookup strategies: exact byte-offset match and (name, arg_count) fallback.
38/// Built once per file when the AST is cached, reused on every inlay hint request.
39#[derive(Debug, Clone)]
40pub struct HintLookup {
41    /// Primary: byte_offset → CallSite (exact match when AST offsets are fresh).
42    by_offset: HashMap<usize, CallSite>,
43    /// Fallback: (name, arg_count) → CallSite (works even with stale offsets).
44    by_name: HashMap<(String, usize), CallSite>,
45}
46
47impl HintLookup {
48    /// Resolve a callsite to get the declaration id and skip count.
49    ///
50    /// Returns `(decl_id, skip)` where skip is the number of leading
51    /// parameters to skip (1 for `using-for` library calls, 0 otherwise).
52    ///
53    /// For signature help, this uses a relaxed lookup: exact offset first,
54    /// then `(name, arg_count)`, then **name-only** fallback (ignoring arg
55    /// count, since the user may still be typing arguments).
56    pub fn resolve_callsite_with_skip(
57        &self,
58        call_offset: usize,
59        func_name: &str,
60        arg_count: usize,
61    ) -> Option<(NodeId, usize)> {
62        // Try exact match first
63        if let Some(site) = lookup_call_site(self, call_offset, func_name, arg_count) {
64            return Some((site.decl_id, site.info.skip));
65        }
66        // Fallback: match by name only (any arg count)
67        self.by_name
68            .iter()
69            .find(|((name, _), _)| name == func_name)
70            .map(|(_, site)| (site.decl_id, site.info.skip))
71    }
72
73    /// Resolve callsite parameter info for hover.
74    ///
75    /// Given a call's byte offset (from tree-sitter), the function name,
76    /// the argument count, and the 0-based argument index, returns a
77    /// `ResolvedCallSite` with the parameter name and declaration id.
78    pub fn resolve_callsite_param(
79        &self,
80        call_offset: usize,
81        func_name: &str,
82        arg_count: usize,
83        arg_index: usize,
84    ) -> Option<ResolvedCallSite> {
85        let site = lookup_call_site(self, call_offset, func_name, arg_count)?;
86        let param_idx = arg_index + site.info.skip;
87        if param_idx >= site.info.names.len() {
88            return None;
89        }
90        let param_name = &site.info.names[param_idx];
91        if param_name.is_empty() {
92            return None;
93        }
94        Some(ResolvedCallSite {
95            param_name: param_name.clone(),
96            decl_id: site.decl_id,
97        })
98    }
99}
100
101/// Pre-computed hint lookups for all files, keyed by absolutePath.
102/// Built once in `CachedBuild::new()`, reused on every inlay hint request.
103pub type HintIndex = HashMap<AbsPath, HintLookup>;
104
105/// Build the hint index for all files from the AST sources.
106/// Pre-computed constructor info for `new ContractName(args)` resolution.
107/// Maps contract declaration ID → (constructor declaration ID, contract name, param names).
108#[derive(Debug, Clone)]
109pub struct ConstructorInfo {
110    pub constructor_id: NodeId,
111    pub contract_name: String,
112    pub param_names: Vec<String>,
113}
114
115/// Index of contract ID → constructor info, built from the typed `decl_index`.
116pub type ConstructorIndex = HashMap<NodeId, ConstructorInfo>;
117
118/// Build the constructor index from the `decl_index`.
119///
120/// For every constructor in the decl_index, record contract ID → constructor info.
121pub fn build_constructor_index(
122    decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
123) -> ConstructorIndex {
124    let mut index = HashMap::with_capacity(decl_index.len() / 8);
125    for (id, decl) in decl_index {
126        if decl.is_constructor()
127            && let Some(scope) = decl.scope()
128        {
129            let scope = NodeId(scope);
130            let names = decl.param_names().unwrap_or_default();
131            // Look up contract name from the scope's decl entry
132            let contract_name = decl_index
133                .get(&scope)
134                .map(|d| d.name().to_string())
135                .unwrap_or_default();
136            index.insert(
137                scope,
138                ConstructorInfo {
139                    constructor_id: *id,
140                    contract_name,
141                    param_names: names,
142                },
143            );
144        }
145    }
146    index
147}
148
149/// Called once in `CachedBuild::new()`.
150///
151/// Accepts the typed `decl_index` and `constructor_index` for O(1) param name
152/// resolution, replacing the previous `id_index: HashMap<u64, Value>`.
153pub fn build_hint_index(
154    sources: &Value,
155    decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
156    constructor_index: &ConstructorIndex,
157) -> HintIndex {
158    let source_count = sources.as_object().map_or(0, |obj| obj.len());
159    let mut hint_index = HashMap::with_capacity(source_count);
160
161    if let Some(obj) = sources.as_object() {
162        for (_, source_data) in obj {
163            if let Some(ast) = source_data.get("ast")
164                && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
165            {
166                let lookup = build_hint_lookup(ast, decl_index, constructor_index);
167                hint_index.insert(AbsPath::new(abs_path), lookup);
168            }
169        }
170    }
171
172    hint_index
173}
174
175/// Generate inlay hints for a given range of source.
176///
177/// Uses tree-sitter on the **live buffer** for argument positions (so hints
178/// follow edits in real time) and the pre-cached hint index for semantic
179/// info (parameter names via `referencedDeclaration`).
180pub fn inlay_hints(
181    build: &CachedBuild,
182    uri: &Url,
183    range: Range,
184    live_source: &[u8],
185) -> Vec<InlayHint> {
186    let path_str = match uri.to_file_path() {
187        Ok(p) => p.to_str().unwrap_or("").to_string(),
188        Err(_) => return vec![],
189    };
190
191    let abs = match build
192        .path_to_abs
193        .iter()
194        .find(|(k, _)| path_str.ends_with(k.as_str()))
195    {
196        Some((_, v)) => v.clone(),
197        None => return vec![],
198    };
199
200    // Use the pre-cached hint lookup for this file
201    let lookup = match build.hint_index.get(&abs) {
202        Some(l) => l,
203        None => return vec![],
204    };
205
206    // Walk tree-sitter on the live buffer for real-time argument positions
207    let source_str = String::from_utf8_lossy(live_source);
208    let tree = match ts_parse(&source_str) {
209        Some(t) => t,
210        None => return vec![],
211    };
212
213    let mut hints = Vec::new();
214    collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
215
216    hints
217}
218
219/// Parse Solidity source with tree-sitter.
220pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
221    let mut parser = Parser::new();
222    parser
223        .set_language(&tree_sitter_solidity::LANGUAGE.into())
224        .expect("failed to load Solidity grammar");
225    parser.parse(source, None)
226}
227
228/// Build both lookup strategies from the AST.
229fn build_hint_lookup(
230    file_ast: &Value,
231    decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
232    constructor_index: &ConstructorIndex,
233) -> HintLookup {
234    let mut lookup = HintLookup {
235        by_offset: HashMap::new(),
236        by_name: HashMap::new(),
237    };
238    collect_ast_calls(file_ast, decl_index, constructor_index, &mut lookup);
239    lookup
240}
241
242/// Parse the `src` field ("offset:length:fileId") and return the byte offset.
243fn parse_src_offset(node: &Value) -> Option<usize> {
244    let src = node.get("src").and_then(|v| v.as_str())?;
245    SourceLoc::parse(src).map(|loc| loc.offset)
246}
247
248/// Recursively walk AST nodes collecting call site info.
249fn collect_ast_calls(
250    node: &Value,
251    decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
252    constructor_index: &ConstructorIndex,
253    lookup: &mut HintLookup,
254) {
255    let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
256
257    match node_type {
258        "FunctionCall" => {
259            if let Some(call_info) = extract_call_info(node, decl_index, constructor_index) {
260                let arg_count = node
261                    .get("arguments")
262                    .and_then(|v| v.as_array())
263                    .map(|a| a.len())
264                    .unwrap_or(0);
265                let site = CallSite {
266                    info: ParamInfo {
267                        names: call_info.params.names,
268                        skip: call_info.params.skip,
269                    },
270                    name: call_info.name,
271                    decl_id: call_info.decl_id,
272                };
273                if let Some(offset) = parse_src_offset(node) {
274                    lookup.by_offset.insert(offset, site.clone());
275                }
276
277                lookup
278                    .by_name
279                    .entry((site.name.clone(), arg_count))
280                    .or_insert(site);
281            }
282        }
283        "EmitStatement" => {
284            if let Some(event_call) = node.get("eventCall")
285                && let Some(call_info) =
286                    extract_call_info(event_call, decl_index, constructor_index)
287            {
288                let arg_count = event_call
289                    .get("arguments")
290                    .and_then(|v| v.as_array())
291                    .map(|a| a.len())
292                    .unwrap_or(0);
293                let site = CallSite {
294                    info: ParamInfo {
295                        names: call_info.params.names,
296                        skip: call_info.params.skip,
297                    },
298                    name: call_info.name,
299                    decl_id: call_info.decl_id,
300                };
301                if let Some(offset) = parse_src_offset(node) {
302                    lookup.by_offset.insert(offset, site.clone());
303                }
304
305                lookup
306                    .by_name
307                    .entry((site.name.clone(), arg_count))
308                    .or_insert(site);
309            }
310        }
311        _ => {}
312    }
313
314    // Recurse into children
315    for key in crate::goto::CHILD_KEYS {
316        if let Some(child) = node.get(*key) {
317            if child.is_array() {
318                if let Some(arr) = child.as_array() {
319                    for item in arr {
320                        collect_ast_calls(item, decl_index, constructor_index, lookup);
321                    }
322                }
323            } else if child.is_object() {
324                collect_ast_calls(child, decl_index, constructor_index, lookup);
325            }
326        }
327    }
328}
329
330/// Resolved call info including the declaration id of the called function/event.
331struct CallInfo {
332    /// Function/event name.
333    name: String,
334    /// Parameter names and skip count.
335    params: ParamInfo,
336    /// The AST node id of the referenced declaration (for DocIndex lookup).
337    decl_id: NodeId,
338}
339
340/// Extract function/event name and parameter info from an AST FunctionCall node.
341fn extract_call_info(
342    node: &Value,
343    decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
344    constructor_index: &ConstructorIndex,
345) -> Option<CallInfo> {
346    let args = node.get("arguments")?.as_array()?;
347    if args.is_empty() {
348        return None;
349    }
350
351    // Skip struct constructors with named args
352    let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
353    if kind == "structConstructorCall"
354        && node
355            .get("names")
356            .and_then(|v| v.as_array())
357            .is_some_and(|n| !n.is_empty())
358    {
359        return None;
360    }
361
362    let expr = node.get("expression")?;
363    let expr_type = expr.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
364
365    // Contract creation: `new Token(args)` — the expression is a NewExpression
366    // whose typeName holds the referencedDeclaration pointing to the contract.
367    // We resolve the constructor from the constructor_index.
368    if expr_type == "NewExpression" {
369        return extract_new_expression_call_info(expr, args.len(), constructor_index);
370    }
371
372    let decl_id = NodeId(expr.get("referencedDeclaration").and_then(|v| v.as_i64())?);
373
374    let decl = decl_index.get(&decl_id)?;
375    let names = decl.param_names()?;
376
377    // Extract the function name from the expression
378    let func_name = extract_function_name(expr)?;
379
380    // Using-for library calls pass the receiver as the implicit first param,
381    // so the AST has one fewer arg than the declaration has params.
382    // Direct library calls (Transaction.addTax) and struct constructors
383    // pass all params explicitly — arg count matches param count.
384    let arg_count = node
385        .get("arguments")
386        .and_then(|v| v.as_array())
387        .map(|a| a.len())
388        .unwrap_or(0);
389    let skip = if is_member_access(expr) && arg_count < names.len() {
390        1
391    } else {
392        0
393    };
394
395    Some(CallInfo {
396        name: func_name,
397        params: ParamInfo { names, skip },
398        decl_id,
399    })
400}
401
402/// Extract call info for a `new ContractName(args)` expression.
403///
404/// The AST represents this as a `FunctionCall` whose `expression` is a
405/// `NewExpression`.  The `NewExpression` has a `typeName` with a
406/// `referencedDeclaration` pointing to the `ContractDefinition`.  We resolve
407/// the constructor via the pre-built `constructor_index`.
408fn extract_new_expression_call_info(
409    new_expr: &Value,
410    _arg_count: usize,
411    constructor_index: &ConstructorIndex,
412) -> Option<CallInfo> {
413    let type_name = new_expr.get("typeName")?;
414    let contract_id = NodeId(
415        type_name
416            .get("referencedDeclaration")
417            .and_then(|v| v.as_i64())?,
418    );
419
420    let info = constructor_index.get(&contract_id)?;
421
422    // Prefer contract name from AST typeName path, fall back to constructor_index
423    let contract_name = type_name
424        .get("pathNode")
425        .and_then(|p| p.get("name"))
426        .and_then(|v| v.as_str())
427        .map(|s| s.to_string())
428        .unwrap_or_else(|| info.contract_name.clone());
429
430    Some(CallInfo {
431        name: contract_name,
432        params: ParamInfo {
433            names: info.param_names.clone(),
434            skip: 0,
435        },
436        decl_id: info.constructor_id,
437    })
438}
439
440/// Extract the function/event name from an AST expression node.
441fn extract_function_name(expr: &Value) -> Option<String> {
442    let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
443    match node_type {
444        "Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
445        "MemberAccess" => expr
446            .get("memberName")
447            .and_then(|v| v.as_str())
448            .map(String::from),
449        "NewExpression" => expr
450            .get("typeName")
451            .and_then(|t| {
452                t.get("pathNode")
453                    .and_then(|p| p.get("name"))
454                    .and_then(|v| v.as_str())
455                    .or_else(|| t.get("name").and_then(|v| v.as_str()))
456            })
457            .map(String::from),
458        _ => None,
459    }
460}
461
462/// Check if expression is a MemberAccess (potential using-for call).
463fn is_member_access(expr: &Value) -> bool {
464    expr.get("nodeType")
465        .and_then(|v| v.as_str())
466        .is_some_and(|t| t == "MemberAccess")
467}
468
469// ── Tree-sitter walk ──────────────────────────────────────────────────────
470
471/// Look up call site info: try exact byte-offset match first, fall back to (name, arg_count).
472fn lookup_call_site<'a>(
473    lookup: &'a HintLookup,
474    offset: usize,
475    name: &str,
476    arg_count: usize,
477) -> Option<&'a CallSite> {
478    // Exact match by byte offset (works when AST is fresh)
479    if let Some(site) = lookup.by_offset.get(&offset)
480        && site.name == name
481    {
482        return Some(site);
483    }
484    // Fallback by (name, arg_count) (works with stale offsets after edits)
485    lookup.by_name.get(&(name.to_string(), arg_count))
486}
487
488/// Recursively walk tree-sitter nodes, emitting hints for calls in the visible range.
489fn collect_ts_hints(
490    node: Node,
491    source: &str,
492    range: &Range,
493    lookup: &HintLookup,
494    hints: &mut Vec<InlayHint>,
495) {
496    // Quick range check — skip nodes entirely outside the visible range
497    let node_start = node.start_position();
498    let node_end = node.end_position();
499    if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
500        return;
501    }
502
503    match node.kind() {
504        "call_expression" => {
505            emit_call_hints(node, source, lookup, hints);
506        }
507        "emit_statement" => {
508            emit_emit_hints(node, source, lookup, hints);
509        }
510        _ => {}
511    }
512
513    // Recurse into children
514    let mut cursor = node.walk();
515    for child in node.children(&mut cursor) {
516        collect_ts_hints(child, source, range, lookup, hints);
517    }
518}
519
520/// Emit parameter hints for a `call_expression` node.
521fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
522    let func_name = match ts_call_function_name(node, source) {
523        Some(n) => n,
524        None => return,
525    };
526
527    let args = ts_call_arguments(node);
528    if args.is_empty() {
529        return;
530    }
531
532    let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
533        Some(s) => s,
534        None => return,
535    };
536
537    emit_param_hints(&args, &site.info, hints);
538}
539
540/// Emit parameter hints for an `emit_statement` node.
541fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
542    let event_name = match ts_emit_event_name(node, source) {
543        Some(n) => n,
544        None => return,
545    };
546
547    let args = ts_call_arguments(node);
548    if args.is_empty() {
549        return;
550    }
551
552    let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
553        Some(s) => s,
554        None => return,
555    };
556
557    emit_param_hints(&args, &site.info, hints);
558}
559
560/// Emit InlayHint items for each argument, using tree-sitter positions.
561fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
562    for (i, arg) in args.iter().enumerate() {
563        let pi = i + info.skip;
564        if pi >= info.names.len() || info.names[pi].is_empty() {
565            continue;
566        }
567
568        let start = arg.start_position();
569        let position = Position::new(start.row as u32, start.column as u32);
570
571        hints.push(InlayHint {
572            position,
573            kind: Some(InlayHintKind::PARAMETER),
574            label: InlayHintLabel::String(format!("{}:", info.names[pi])),
575            text_edits: None,
576            tooltip: None,
577            padding_left: None,
578            padding_right: Some(true),
579            data: None,
580        });
581    }
582}
583
584// ── Tree-sitter helpers ───────────────────────────────────────────────────
585
586/// Get the function name from a `call_expression` node.
587///
588/// For `transfer(...)` → "transfer"
589/// For `PRICE.addTax(...)` → "addTax"
590/// For `router.swap{value: 100}(...)` → "swap"  (value modifier)
591fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
592    let func_expr = node.child_by_field_name("function")?;
593    // The expression wrapper has one named child
594    let inner = first_named_child(func_expr)?;
595    extract_name_from_expr(inner, source)
596}
597
598/// Recursively extract the function/event name from an expression node.
599///
600/// tree-sitter-solidity parses `foo{value: 100}(args)` as a `call_expression`
601/// whose `function` field is a `struct_expression` (because the grammar lacks
602/// `function_call_options_expression`). We handle this by drilling into the
603/// struct_expression's `type` field to find the real function name.
604fn extract_name_from_expr<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
605    match node.kind() {
606        "identifier" => Some(&source[node.byte_range()]),
607        "member_expression" => {
608            let prop = node.child_by_field_name("property")?;
609            Some(&source[prop.byte_range()])
610        }
611        "struct_expression" => {
612            // foo{value: 100} → type field holds the actual callee expression
613            let type_expr = node.child_by_field_name("type")?;
614            extract_name_from_expr(type_expr, source)
615        }
616        "new_expression" => {
617            // new Token(...) → the `name` field holds the type_name
618            ts_new_expression_name(node, source)
619        }
620        "expression" => {
621            // tree-sitter wraps many nodes in an `expression` wrapper — unwrap it
622            let inner = first_named_child(node)?;
623            extract_name_from_expr(inner, source)
624        }
625        _ => None,
626    }
627}
628
629/// Get the event name from an `emit_statement` node.
630fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
631    let name_expr = node.child_by_field_name("name")?;
632    let inner = first_named_child(name_expr)?;
633    match inner.kind() {
634        "identifier" => Some(&source[inner.byte_range()]),
635        "member_expression" => {
636            let prop = inner.child_by_field_name("property")?;
637            Some(&source[prop.byte_range()])
638        }
639        _ => None,
640    }
641}
642
643/// Get the contract name from a `new_expression` node.
644///
645/// For `new Token(...)` → "Token"
646/// For `new Router(...)` → "Router"
647fn ts_new_expression_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
648    let name_node = node.child_by_field_name("name")?;
649    // The `name` field is a type_name; extract the identifier text from it
650    if name_node.kind() == "user_defined_type" || name_node.kind() == "type_name" {
651        // May have a single identifier child
652        let mut cursor = name_node.walk();
653        for child in name_node.children(&mut cursor) {
654            if child.kind() == "identifier" {
655                return Some(&source[child.byte_range()]);
656            }
657        }
658        // Fallback: use the full text of the name node
659        Some(&source[name_node.byte_range()])
660    } else {
661        Some(&source[name_node.byte_range()])
662    }
663}
664
665/// Collect `call_argument` children from a node (works for both
666/// `call_expression` and `emit_statement` since `_call_arguments` is hidden).
667fn ts_call_arguments(node: Node) -> Vec<Node> {
668    let mut args = Vec::new();
669    let mut cursor = node.walk();
670    for child in node.children(&mut cursor) {
671        if child.kind() == "call_argument" {
672            args.push(child);
673        }
674    }
675    args
676}
677
678/// Get the first named child of a node.
679fn first_named_child(node: Node) -> Option<Node> {
680    let mut cursor = node.walk();
681    node.children(&mut cursor).find(|c| c.is_named())
682}
683
684/// Result of finding the enclosing call site at a byte position via tree-sitter.
685pub struct TsCallContext<'a> {
686    /// The function/event name.
687    pub name: &'a str,
688    /// 0-based index of the argument the cursor is on.
689    pub arg_index: usize,
690    /// Total number of arguments in the call.
691    pub arg_count: usize,
692    /// Start byte of the call_expression/emit_statement node (for HintIndex lookup).
693    pub call_start_byte: usize,
694    /// True when context is a mapping/array index access (`name[key]`)
695    /// rather than a function/event call (`name(args)`).
696    pub is_index_access: bool,
697}
698
699/// Find the enclosing `call_expression` or `emit_statement` for a given byte
700/// position in the live buffer using tree-sitter.
701///
702/// Returns `None` if the position is not inside a call argument.
703pub fn ts_find_call_at_byte<'a>(
704    root: tree_sitter::Node<'a>,
705    source: &'a str,
706    byte_pos: usize,
707) -> Option<TsCallContext<'a>> {
708    // Find the deepest node containing byte_pos
709    let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
710
711    // Walk up the tree to find a call_argument parent
712    loop {
713        if node.kind() == "call_argument" {
714            break;
715        }
716        node = node.parent()?;
717    }
718
719    // The call_argument's parent should be the call_expression or emit_statement
720    let call_node = node.parent()?;
721    let args = ts_call_arguments(call_node);
722    let arg_index = args.iter().position(|a| a.id() == node.id())?;
723
724    match call_node.kind() {
725        "call_expression" => {
726            let name = ts_call_function_name(call_node, source)?;
727            Some(TsCallContext {
728                name,
729                arg_index,
730                arg_count: args.len(),
731                call_start_byte: call_node.start_byte(),
732                is_index_access: false,
733            })
734        }
735        "emit_statement" => {
736            let name = ts_emit_event_name(call_node, source)?;
737            Some(TsCallContext {
738                name,
739                arg_index,
740                arg_count: args.len(),
741                call_start_byte: call_node.start_byte(),
742                is_index_access: false,
743            })
744        }
745        _ => None,
746    }
747}
748
749/// Find the enclosing call for signature help at a byte position.
750///
751/// Unlike `ts_find_call_at_byte`, this handles:
752/// - Cursor right after `(` with no arguments yet
753/// - Cursor between `,` and next argument
754/// - Incomplete calls without closing `)`
755///
756/// Falls back to text-based scanning when tree-sitter can't produce a
757/// `call_expression` (e.g. broken syntax during typing).
758pub fn ts_find_call_for_signature<'a>(
759    root: tree_sitter::Node<'a>,
760    source: &'a str,
761    byte_pos: usize,
762) -> Option<TsCallContext<'a>> {
763    // First try the normal path (cursor is on an argument)
764    if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
765        return Some(ctx);
766    }
767
768    // Walk up from the deepest node looking for a call_expression or array_access
769    let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
770    loop {
771        match node.kind() {
772            "call_expression" => {
773                let name = ts_call_function_name(node, source)?;
774                let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
775                let args = ts_call_arguments(node);
776                let arg_count = args.len().max(arg_index + 1);
777                return Some(TsCallContext {
778                    name,
779                    arg_index,
780                    arg_count,
781                    call_start_byte: node.start_byte(),
782                    is_index_access: false,
783                });
784            }
785            "emit_statement" => {
786                let name = ts_emit_event_name(node, source)?;
787                let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
788                let args = ts_call_arguments(node);
789                let arg_count = args.len().max(arg_index + 1);
790                return Some(TsCallContext {
791                    name,
792                    arg_index,
793                    arg_count,
794                    call_start_byte: node.start_byte(),
795                    is_index_access: false,
796                });
797            }
798            "array_access" => {
799                // Mapping/array index access: `name[key]`
800                let base_node = node.child_by_field_name("base")?;
801                // For member_expression (e.g. self.orders), use property name;
802                // for plain identifier, use the identifier text.
803                let name_node = if base_node.kind() == "member_expression" {
804                    base_node
805                        .child_by_field_name("property")
806                        .unwrap_or(base_node)
807                } else {
808                    base_node
809                };
810                let name = &source[name_node.byte_range()];
811                return Some(TsCallContext {
812                    name,
813                    arg_index: 0,
814                    arg_count: 1,
815                    call_start_byte: node.start_byte(),
816                    is_index_access: true,
817                });
818            }
819            "source_file" => break,
820            _ => {
821                node = node.parent()?;
822            }
823        }
824    }
825
826    // Fallback: scan backwards from cursor for `identifier(` pattern
827    if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
828        return Some(ctx);
829    }
830
831    // Fallback: scan backwards for `identifier[` (mapping/array access)
832    find_index_by_text_scan(source, byte_pos)
833}
834
835/// Scan backwards from `byte_pos` to find an enclosing `name(` call.
836///
837/// Looks for the nearest unmatched `(` before the cursor, then extracts
838/// the function name preceding it. Counts commas at depth 1 to determine
839/// the active argument index.
840fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
841    let before = &source[..byte_pos.min(source.len())];
842
843    // Find the nearest unmatched `(` by scanning backwards
844    let mut depth: i32 = 0;
845    let mut paren_pos = None;
846    for (i, ch) in before.char_indices().rev() {
847        match ch {
848            ')' => depth += 1,
849            '(' => {
850                if depth == 0 {
851                    paren_pos = Some(i);
852                    break;
853                }
854                depth -= 1;
855            }
856            _ => {}
857        }
858    }
859    let paren_pos = paren_pos?;
860
861    // Extract the function name before the `(`
862    // If there's a `{...}` block before the paren (e.g. `swap{value: 100}(`),
863    // skip over it to find the real function name.
864    let mut scan_end = paren_pos;
865    let before_paren = source[..scan_end].trim_end();
866    if before_paren.ends_with('}') {
867        // Skip the `{...}` block by finding the matching `{`
868        let mut brace_depth: i32 = 0;
869        for (i, ch) in before_paren.char_indices().rev() {
870            match ch {
871                '}' => brace_depth += 1,
872                '{' => {
873                    brace_depth -= 1;
874                    if brace_depth == 0 {
875                        scan_end = i;
876                        break;
877                    }
878                }
879                _ => {}
880            }
881        }
882    }
883    let before_name = &source[..scan_end];
884    let name_end = before_name.trim_end().len();
885    let name_start = before_name[..name_end]
886        .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
887        .map(|i| i + 1)
888        .unwrap_or(0);
889    // For member expressions like `router.swap`, take only the part after the last dot
890    let raw_name = &source[name_start..name_end];
891    let name = match raw_name.rfind('.') {
892        Some(dot) => &raw_name[dot + 1..],
893        None => raw_name,
894    };
895
896    if name.is_empty() || !name.chars().next()?.is_alphabetic() {
897        return None;
898    }
899
900    // Count commas between `(` and cursor at depth 0
901    let arg_index = count_commas_before(source, paren_pos, byte_pos);
902
903    Some(TsCallContext {
904        name,
905        arg_index,
906        arg_count: arg_index + 1,
907        call_start_byte: name_start,
908        is_index_access: false,
909    })
910}
911
912/// Scan backwards from `byte_pos` to find an enclosing `name[` index access.
913///
914/// Similar to `find_call_by_text_scan` but for `[` brackets instead of `(`.
915/// Returns a context with `is_index_access = true`.
916fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
917    let before = &source[..byte_pos.min(source.len())];
918
919    // Find the nearest unmatched `[` by scanning backwards
920    let mut depth: i32 = 0;
921    let mut bracket_pos = None;
922    for (i, c) in before.char_indices().rev() {
923        match c {
924            ']' => depth += 1,
925            '[' => {
926                if depth == 0 {
927                    bracket_pos = Some(i);
928                    break;
929                }
930                depth -= 1;
931            }
932            _ => {}
933        }
934    }
935    let bracket_pos = bracket_pos?;
936
937    // Extract the identifier name before the `[`
938    let before_bracket = &source[..bracket_pos];
939    let name_end = before_bracket.trim_end().len();
940    let name_start = before_bracket[..name_end]
941        .rfind(|c: char| !c.is_alphanumeric() && c != '_')
942        .map(|i| i + 1)
943        .unwrap_or(0);
944    let name = &source[name_start..name_end];
945
946    if name.is_empty() || !name.chars().next()?.is_alphabetic() {
947        return None;
948    }
949
950    Some(TsCallContext {
951        name,
952        arg_index: 0,
953        arg_count: 1,
954        call_start_byte: name_start,
955        is_index_access: true,
956    })
957}
958
959/// Count commas at depth 1 between `start` and `byte_pos` to determine argument index.
960fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
961    let end = byte_pos.min(source.len());
962    let text = &source[start..end];
963
964    let mut count = 0;
965    let mut depth = 0;
966    let mut found_open = false;
967    for ch in text.chars() {
968        match ch {
969            '(' if !found_open => {
970                found_open = true;
971                depth = 1;
972            }
973            '(' => depth += 1,
974            ')' => depth -= 1,
975            ',' if found_open && depth == 1 => count += 1,
976            _ => {}
977        }
978    }
979    count
980}
981
982// ── AST helpers ──────────────────────────────────────────────────────────
983
984#[cfg(test)]
985mod tests {
986    use super::*;
987
988    #[test]
989    fn test_ts_call_function_name() {
990        let source = r#"
991contract Foo {
992    function bar(uint x) public {}
993    function test() public {
994        bar(42);
995    }
996}
997"#;
998        let tree = ts_parse(source).unwrap();
999        let mut found = Vec::new();
1000        find_calls(tree.root_node(), source, &mut found);
1001        assert_eq!(found.len(), 1);
1002        assert_eq!(found[0], "bar");
1003    }
1004
1005    #[test]
1006    fn test_ts_member_call_name() {
1007        let source = r#"
1008contract Foo {
1009    function test() public {
1010        PRICE.addTax(TAX, TAX_BASE);
1011    }
1012}
1013"#;
1014        let tree = ts_parse(source).unwrap();
1015        let mut found = Vec::new();
1016        find_calls(tree.root_node(), source, &mut found);
1017        assert_eq!(found.len(), 1);
1018        assert_eq!(found[0], "addTax");
1019    }
1020
1021    #[test]
1022    fn test_ts_call_with_value_modifier() {
1023        let source = r#"
1024contract Foo {
1025    function test() public {
1026        router.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
1027    }
1028}
1029"#;
1030        let tree = ts_parse(source).unwrap();
1031        let mut found = Vec::new();
1032        find_calls(tree.root_node(), source, &mut found);
1033        assert_eq!(found.len(), 1, "should find one call");
1034        assert_eq!(
1035            found[0], "swap",
1036            "should extract 'swap' through struct_expression"
1037        );
1038    }
1039
1040    #[test]
1041    fn test_ts_call_simple_with_value_modifier() {
1042        let source = r#"
1043contract Foo {
1044    function test() public {
1045        foo{value: 1 ether}(42);
1046    }
1047}
1048"#;
1049        let tree = ts_parse(source).unwrap();
1050        let mut found = Vec::new();
1051        find_calls(tree.root_node(), source, &mut found);
1052        assert_eq!(found.len(), 1, "should find one call");
1053        assert_eq!(
1054            found[0], "foo",
1055            "should extract 'foo' through struct_expression"
1056        );
1057    }
1058
1059    #[test]
1060    fn test_ts_call_with_gas_modifier() {
1061        let source = r#"
1062contract Foo {
1063    function test() public {
1064        addr.call{gas: 5000, value: 1 ether}("");
1065    }
1066}
1067"#;
1068        let tree = ts_parse(source).unwrap();
1069        let mut found = Vec::new();
1070        find_calls(tree.root_node(), source, &mut found);
1071        assert_eq!(found.len(), 1, "should find one call");
1072        assert_eq!(
1073            found[0], "call",
1074            "should extract 'call' through struct_expression"
1075        );
1076    }
1077
1078    #[test]
1079    fn test_find_call_by_text_scan_with_value_modifier() {
1080        // Simulate cursor inside args of `router.swap{value: 100}(arg1, |)`
1081        let source = "router.swap{value: 100}(nativeKey, SWAP_PARAMS)";
1082        // Place cursor after comma: position after "nativeKey, "
1083        let byte_pos = source.find("SWAP_PARAMS").unwrap();
1084        let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1085        assert_eq!(ctx.name, "swap");
1086        assert_eq!(ctx.arg_index, 1);
1087    }
1088
1089    #[test]
1090    fn test_find_call_by_text_scan_simple_value_modifier() {
1091        let source = "foo{value: 1 ether}(42)";
1092        let byte_pos = source.find("42").unwrap();
1093        let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1094        assert_eq!(ctx.name, "foo");
1095        assert_eq!(ctx.arg_index, 0);
1096    }
1097
1098    #[test]
1099    fn test_ts_emit_event_name() {
1100        let source = r#"
1101contract Foo {
1102    event Purchase(address buyer, uint256 price);
1103    function test() public {
1104        emit Purchase(msg.sender, 100);
1105    }
1106}
1107"#;
1108        let tree = ts_parse(source).unwrap();
1109        let mut found = Vec::new();
1110        find_emits(tree.root_node(), source, &mut found);
1111        assert_eq!(found.len(), 1);
1112        assert_eq!(found[0], "Purchase");
1113    }
1114
1115    #[test]
1116    fn test_ts_new_expression_name() {
1117        let source = r#"
1118contract Token {
1119    constructor(string memory _name, uint256 _supply) {}
1120}
1121contract Factory {
1122    function create() public {
1123        Token t = new Token("MyToken", 1000);
1124    }
1125}
1126"#;
1127        let tree = ts_parse(source).unwrap();
1128        let mut found = Vec::new();
1129        find_new_exprs(tree.root_node(), source, &mut found);
1130        assert_eq!(found.len(), 1);
1131        assert_eq!(found[0], "Token");
1132    }
1133
1134    #[test]
1135    fn test_ts_new_expression_arguments() {
1136        // tree-sitter wraps `new Token(args)` as a call_expression whose
1137        // callee is a new_expression. The call_argument nodes live on the
1138        // call_expression, so we count them there.
1139        let source = r#"
1140contract Router {
1141    constructor(address _manager, address _hook) {}
1142}
1143contract Factory {
1144    function create() public {
1145        Router r = new Router(address(this), address(0));
1146    }
1147}
1148"#;
1149        let tree = ts_parse(source).unwrap();
1150        let mut arg_counts = Vec::new();
1151        find_call_arg_counts(tree.root_node(), &mut arg_counts);
1152        // call_expression for `new Router(...)` has 2 args
1153        assert_eq!(arg_counts, vec![2]);
1154    }
1155
1156    #[test]
1157    fn test_ts_find_call_at_byte_new_expression() {
1158        let source = r#"
1159contract Token {
1160    constructor(string memory _name, uint256 _supply) {}
1161}
1162contract Factory {
1163    function create() public {
1164        Token t = new Token("MyToken", 1000);
1165    }
1166}
1167"#;
1168        let tree = ts_parse(source).unwrap();
1169        let pos = source.find("1000").unwrap();
1170        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1171        assert_eq!(ctx.name, "Token");
1172        assert_eq!(ctx.arg_index, 1);
1173        assert_eq!(ctx.arg_count, 2);
1174    }
1175
1176    #[test]
1177    fn test_ts_find_call_at_byte_new_expression_first_arg() {
1178        let source = r#"
1179contract Token {
1180    constructor(string memory _name, uint256 _supply) {}
1181}
1182contract Factory {
1183    function create() public {
1184        Token t = new Token("MyToken", 1000);
1185    }
1186}
1187"#;
1188        let tree = ts_parse(source).unwrap();
1189        let pos = source.find("\"MyToken\"").unwrap();
1190        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1191        assert_eq!(ctx.name, "Token");
1192        assert_eq!(ctx.arg_index, 0);
1193        assert_eq!(ctx.arg_count, 2);
1194    }
1195
1196    #[test]
1197    fn test_extract_new_expression_call_info() {
1198        // Simulate `new Token("MyToken", 1000)` using a ConstructorIndex
1199        let new_expr: Value = serde_json::json!({
1200            "nodeType": "NewExpression",
1201            "typeName": {
1202                "nodeType": "UserDefinedTypeName",
1203                "referencedDeclaration": 22,
1204                "pathNode": {
1205                    "name": "Token"
1206                }
1207            }
1208        });
1209
1210        let mut constructor_index = ConstructorIndex::new();
1211        constructor_index.insert(
1212            NodeId(22),
1213            ConstructorInfo {
1214                constructor_id: NodeId(21),
1215                contract_name: "Token".to_string(),
1216                param_names: vec!["_name".to_string(), "_supply".to_string()],
1217            },
1218        );
1219
1220        let info = extract_new_expression_call_info(&new_expr, 2, &constructor_index).unwrap();
1221        assert_eq!(info.name, "Token");
1222        assert_eq!(info.params.names, vec!["_name", "_supply"]);
1223        assert_eq!(info.params.skip, 0);
1224        assert_eq!(info.decl_id, NodeId(21));
1225    }
1226
1227    #[test]
1228    fn test_ts_call_arguments_count() {
1229        let source = r#"
1230contract Foo {
1231    function bar(uint x, uint y) public {}
1232    function test() public {
1233        bar(1, 2);
1234    }
1235}
1236"#;
1237        let tree = ts_parse(source).unwrap();
1238        let mut arg_counts = Vec::new();
1239        find_call_arg_counts(tree.root_node(), &mut arg_counts);
1240        assert_eq!(arg_counts, vec![2]);
1241    }
1242
1243    #[test]
1244    fn test_ts_argument_positions_follow_live_buffer() {
1245        // Simulate an edited buffer with extra whitespace
1246        let source = r#"
1247contract Foo {
1248    function bar(uint x, uint y) public {}
1249    function test() public {
1250        bar(
1251            1,
1252            2
1253        );
1254    }
1255}
1256"#;
1257        let tree = ts_parse(source).unwrap();
1258        let mut positions = Vec::new();
1259        find_arg_positions(tree.root_node(), &mut positions);
1260        // First arg "1" is on line 5 (0-indexed), second "2" on line 6
1261        assert_eq!(positions.len(), 2);
1262        assert_eq!(positions[0].0, 5); // row of "1"
1263        assert_eq!(positions[1].0, 6); // row of "2"
1264    }
1265
1266    // Test helpers
1267
1268    fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1269        if node.kind() == "call_expression"
1270            && let Some(name) = ts_call_function_name(node, source)
1271        {
1272            out.push(name);
1273        }
1274        let mut cursor = node.walk();
1275        for child in node.children(&mut cursor) {
1276            find_calls(child, source, out);
1277        }
1278    }
1279
1280    fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1281        if node.kind() == "emit_statement"
1282            && let Some(name) = ts_emit_event_name(node, source)
1283        {
1284            out.push(name);
1285        }
1286        let mut cursor = node.walk();
1287        for child in node.children(&mut cursor) {
1288            find_emits(child, source, out);
1289        }
1290    }
1291
1292    fn find_new_exprs<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1293        if node.kind() == "new_expression"
1294            && let Some(name) = ts_new_expression_name(node, source)
1295        {
1296            out.push(name);
1297        }
1298        let mut cursor = node.walk();
1299        for child in node.children(&mut cursor) {
1300            find_new_exprs(child, source, out);
1301        }
1302    }
1303
1304    fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
1305        if node.kind() == "call_expression" {
1306            out.push(ts_call_arguments(node).len());
1307        }
1308        let mut cursor = node.walk();
1309        for child in node.children(&mut cursor) {
1310            find_call_arg_counts(child, out);
1311        }
1312    }
1313
1314    fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
1315        if node.kind() == "call_expression" {
1316            for arg in ts_call_arguments(node) {
1317                let p = arg.start_position();
1318                out.push((p.row, p.column));
1319            }
1320        }
1321        let mut cursor = node.walk();
1322        for child in node.children(&mut cursor) {
1323            find_arg_positions(child, out);
1324        }
1325    }
1326
1327    #[test]
1328    fn test_ts_find_call_at_byte_first_arg() {
1329        let source = r#"
1330contract Foo {
1331    function bar(uint x, uint y) public {}
1332    function test() public {
1333        bar(42, 99);
1334    }
1335}
1336"#;
1337        let tree = ts_parse(source).unwrap();
1338        // "42" is the first argument — find its byte offset
1339        let pos_42 = source.find("42").unwrap();
1340        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1341        assert_eq!(ctx.name, "bar");
1342        assert_eq!(ctx.arg_index, 0);
1343        assert_eq!(ctx.arg_count, 2);
1344    }
1345
1346    #[test]
1347    fn test_ts_find_call_at_byte_second_arg() {
1348        let source = r#"
1349contract Foo {
1350    function bar(uint x, uint y) public {}
1351    function test() public {
1352        bar(42, 99);
1353    }
1354}
1355"#;
1356        let tree = ts_parse(source).unwrap();
1357        let pos_99 = source.find("99").unwrap();
1358        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1359        assert_eq!(ctx.name, "bar");
1360        assert_eq!(ctx.arg_index, 1);
1361        assert_eq!(ctx.arg_count, 2);
1362    }
1363
1364    #[test]
1365    fn test_ts_find_call_at_byte_outside_call_returns_none() {
1366        let source = r#"
1367contract Foo {
1368    function bar(uint x) public {}
1369    function test() public {
1370        uint z = 10;
1371        bar(42);
1372    }
1373}
1374"#;
1375        let tree = ts_parse(source).unwrap();
1376        // "10" is a local variable assignment, not a call argument
1377        let pos_10 = source.find("10").unwrap();
1378        assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1379    }
1380
1381    #[test]
1382    fn test_ts_find_call_at_byte_member_call() {
1383        let source = r#"
1384contract Foo {
1385    function test() public {
1386        PRICE.addTax(TAX, TAX_BASE);
1387    }
1388}
1389"#;
1390        let tree = ts_parse(source).unwrap();
1391        let pos_tax = source.find("TAX,").unwrap();
1392        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1393        assert_eq!(ctx.name, "addTax");
1394        assert_eq!(ctx.arg_index, 0);
1395        assert_eq!(ctx.arg_count, 2);
1396    }
1397
1398    #[test]
1399    fn test_ts_find_call_at_byte_emit_statement() {
1400        let source = r#"
1401contract Foo {
1402    event Purchase(address buyer, uint256 price);
1403    function test() public {
1404        emit Purchase(msg.sender, 100);
1405    }
1406}
1407"#;
1408        let tree = ts_parse(source).unwrap();
1409        let pos_100 = source.find("100").unwrap();
1410        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1411        assert_eq!(ctx.name, "Purchase");
1412        assert_eq!(ctx.arg_index, 1);
1413        assert_eq!(ctx.arg_count, 2);
1414    }
1415
1416    #[test]
1417    fn test_ts_find_call_at_byte_multiline() {
1418        let source = r#"
1419contract Foo {
1420    function bar(uint x, uint y, uint z) public {}
1421    function test() public {
1422        bar(
1423            1,
1424            2,
1425            3
1426        );
1427    }
1428}
1429"#;
1430        let tree = ts_parse(source).unwrap();
1431        // Find "2" — the second argument on its own line
1432        // Need to be careful since "2" appears in the source in multiple places
1433        let pos_2 = source.find("            2").unwrap() + 12; // skip whitespace
1434        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1435        assert_eq!(ctx.name, "bar");
1436        assert_eq!(ctx.arg_index, 1);
1437        assert_eq!(ctx.arg_count, 3);
1438    }
1439
1440    #[test]
1441    fn test_resolve_callsite_param_basic() {
1442        // Build a HintLookup manually with a known call site
1443        let mut lookup = HintLookup {
1444            by_offset: HashMap::new(),
1445            by_name: HashMap::new(),
1446        };
1447        lookup.by_name.insert(
1448            ("transfer".to_string(), 2),
1449            CallSite {
1450                info: ParamInfo {
1451                    names: vec!["to".to_string(), "amount".to_string()],
1452                    skip: 0,
1453                },
1454                name: "transfer".to_string(),
1455                decl_id: NodeId(42),
1456            },
1457        );
1458
1459        // Resolve first argument
1460        let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1461        assert_eq!(result.param_name, "to");
1462        assert_eq!(result.decl_id, NodeId(42));
1463
1464        // Resolve second argument
1465        let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1466        assert_eq!(result.param_name, "amount");
1467        assert_eq!(result.decl_id, NodeId(42));
1468    }
1469
1470    #[test]
1471    fn test_resolve_callsite_param_with_skip() {
1472        // Simulate a using-for library call where skip=1
1473        let mut lookup = HintLookup {
1474            by_offset: HashMap::new(),
1475            by_name: HashMap::new(),
1476        };
1477        lookup.by_name.insert(
1478            ("addTax".to_string(), 2),
1479            CallSite {
1480                info: ParamInfo {
1481                    names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1482                    skip: 1,
1483                },
1484                name: "addTax".to_string(),
1485                decl_id: NodeId(99),
1486            },
1487        );
1488
1489        // First arg maps to param index 1 (skip=1), which is "tax"
1490        let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1491        assert_eq!(result.param_name, "tax");
1492
1493        // Second arg maps to param index 2, which is "base"
1494        let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1495        assert_eq!(result.param_name, "base");
1496    }
1497
1498    #[test]
1499    fn test_resolve_callsite_param_out_of_bounds() {
1500        let mut lookup = HintLookup {
1501            by_offset: HashMap::new(),
1502            by_name: HashMap::new(),
1503        };
1504        lookup.by_name.insert(
1505            ("foo".to_string(), 1),
1506            CallSite {
1507                info: ParamInfo {
1508                    names: vec!["x".to_string()],
1509                    skip: 0,
1510                },
1511                name: "foo".to_string(),
1512                decl_id: NodeId(1),
1513            },
1514        );
1515
1516        // Arg index 1 is out of bounds for a single-param function
1517        assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1518    }
1519
1520    #[test]
1521    fn test_resolve_callsite_param_unknown_function() {
1522        let lookup = HintLookup {
1523            by_offset: HashMap::new(),
1524            by_name: HashMap::new(),
1525        };
1526        assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1527    }
1528
1529    #[test]
1530    fn test_ts_find_call_at_byte_emit_member_access() {
1531        // Simulates: emit ModifyLiquidity(id, msg.sender, params.tickLower, ...);
1532        // Hovering on "tickLower" (the member name in params.tickLower) should
1533        // resolve to arg_index=2 of the ModifyLiquidity emit.
1534        let source = r#"
1535contract Foo {
1536    event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1537    function test() public {
1538        emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1539    }
1540}
1541"#;
1542        let tree = ts_parse(source).unwrap();
1543        // Find "tickLower" inside "params.tickLower" — the first occurrence after "params."
1544        let params_tick = source.find("params.tickLower,").unwrap();
1545        let tick_lower_pos = params_tick + "params.".len(); // points at "tickLower"
1546
1547        let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1548        assert_eq!(ctx.name, "ModifyLiquidity");
1549        assert_eq!(
1550            ctx.arg_index, 2,
1551            "params.tickLower is the 3rd argument (index 2)"
1552        );
1553        assert_eq!(ctx.arg_count, 4);
1554    }
1555
1556    #[test]
1557    fn test_ts_find_call_at_byte_member_access_on_property() {
1558        // Hovering on "sender" in "msg.sender" as an argument
1559        let source = r#"
1560contract Foo {
1561    event Transfer(address from, address to);
1562    function test() public {
1563        emit Transfer(msg.sender, addr);
1564    }
1565}
1566"#;
1567        let tree = ts_parse(source).unwrap();
1568        let sender_pos = source.find("sender").unwrap();
1569        let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1570        assert_eq!(ctx.name, "Transfer");
1571        assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1572    }
1573
1574    #[test]
1575    fn test_ts_find_call_at_byte_emit_all_args() {
1576        // Verify each argument position in an emit with member accesses
1577        let source = r#"
1578contract Foo {
1579    event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1580    function test() public {
1581        emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1582    }
1583}
1584"#;
1585        let tree = ts_parse(source).unwrap();
1586
1587        // arg 0: "id"
1588        let pos_id = source.find("(id,").unwrap() + 1;
1589        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1590        assert_eq!(ctx.name, "ModifyLiquidity");
1591        assert_eq!(ctx.arg_index, 0);
1592
1593        // arg 1: "msg.sender" — hover on "msg"
1594        let pos_msg = source.find("msg.sender").unwrap();
1595        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1596        assert_eq!(ctx.arg_index, 1);
1597
1598        // arg 2: "params.tickLower" — hover on "tickLower"
1599        let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1600        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1601        assert_eq!(ctx.arg_index, 2);
1602
1603        // arg 3: "params.tickUpper" — hover on "params"
1604        let pos_tu = source.find("params.tickUpper").unwrap();
1605        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1606        assert_eq!(ctx.arg_index, 3);
1607    }
1608
1609    #[test]
1610    fn test_ts_find_call_at_byte_nested_call_arg() {
1611        // When an argument is itself a function call, hovering inside
1612        // the inner call should find the inner call, not the outer.
1613        let source = r#"
1614contract Foo {
1615    function inner(uint x) public returns (uint) {}
1616    function outer(uint a, uint b) public {}
1617    function test() public {
1618        outer(inner(42), 99);
1619    }
1620}
1621"#;
1622        let tree = ts_parse(source).unwrap();
1623
1624        // "42" is an arg to inner(), not outer()
1625        let pos_42 = source.find("42").unwrap();
1626        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1627        assert_eq!(ctx.name, "inner");
1628        assert_eq!(ctx.arg_index, 0);
1629
1630        // "99" is an arg to outer()
1631        let pos_99 = source.find("99").unwrap();
1632        let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1633        assert_eq!(ctx.name, "outer");
1634        assert_eq!(ctx.arg_index, 1);
1635    }
1636
1637    #[test]
1638    fn test_ts_find_call_for_signature_incomplete_call() {
1639        // Cursor right after `(` with no arguments yet
1640        let source = r#"
1641contract Foo {
1642    function bar(uint x, uint y) public {}
1643    function test() public {
1644        bar(
1645    }
1646}
1647"#;
1648        let tree = ts_parse(source).unwrap();
1649        let pos = source.find("bar(").unwrap() + 4;
1650        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1651        assert_eq!(ctx.name, "bar");
1652        assert_eq!(ctx.arg_index, 0);
1653    }
1654
1655    #[test]
1656    fn test_ts_find_call_for_signature_after_comma() {
1657        // Cursor right after `,` — on second argument
1658        let source = r#"
1659contract Foo {
1660    function bar(uint x, uint y) public {}
1661    function test() public {
1662        bar(42,
1663    }
1664}
1665"#;
1666        let tree = ts_parse(source).unwrap();
1667        let pos = source.find("42,").unwrap() + 3;
1668        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1669        assert_eq!(ctx.name, "bar");
1670        assert_eq!(ctx.arg_index, 1);
1671    }
1672
1673    #[test]
1674    fn test_ts_find_call_for_signature_complete_call() {
1675        // Normal complete call still works
1676        let source = r#"
1677contract Foo {
1678    function bar(uint x, uint y) public {}
1679    function test() public {
1680        bar(42, 99);
1681    }
1682}
1683"#;
1684        let tree = ts_parse(source).unwrap();
1685        let pos = source.find("42").unwrap();
1686        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1687        assert_eq!(ctx.name, "bar");
1688        assert_eq!(ctx.arg_index, 0);
1689    }
1690
1691    #[test]
1692    fn test_ts_find_call_for_signature_member_call() {
1693        // Member access call like PRICE.addTax(
1694        let source = r#"
1695contract Foo {
1696    function test() public {
1697        PRICE.addTax(
1698    }
1699}
1700"#;
1701        let tree = ts_parse(source).unwrap();
1702        let pos = source.find("addTax(").unwrap() + 7;
1703        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1704        assert_eq!(ctx.name, "addTax");
1705        assert_eq!(ctx.arg_index, 0);
1706    }
1707
1708    #[test]
1709    fn test_ts_find_call_for_signature_array_access() {
1710        // Mapping index access like orders[orderId]
1711        let source = r#"
1712contract Foo {
1713    mapping(bytes32 => uint256) public orders;
1714    function test() public {
1715        orders[orderId];
1716    }
1717}
1718"#;
1719        let tree = ts_parse(source).unwrap();
1720        // Cursor inside the brackets, on "orderId"
1721        let pos = source.find("[orderId]").unwrap() + 1;
1722        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1723        assert_eq!(ctx.name, "orders");
1724        assert_eq!(ctx.arg_index, 0);
1725        assert!(ctx.is_index_access);
1726    }
1727
1728    #[test]
1729    fn test_ts_find_call_for_signature_array_access_empty() {
1730        // Cursor right after `[` with no key yet
1731        let source = r#"
1732contract Foo {
1733    mapping(bytes32 => uint256) public orders;
1734    function test() public {
1735        orders[
1736    }
1737}
1738"#;
1739        let tree = ts_parse(source).unwrap();
1740        let pos = source.find("orders[").unwrap() + 7;
1741        let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1742        assert_eq!(ctx.name, "orders");
1743        assert!(ctx.is_index_access);
1744    }
1745}