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