Skip to main content

solidity_language_server/
inlay_hints.rs

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