Skip to main content

solidity_language_server/
completion.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use tower_lsp::lsp_types::{
5    CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, Position, Range,
6    TextEdit,
7};
8
9use crate::goto::CHILD_KEYS;
10use crate::hover::build_function_signature;
11use crate::types::{FileId, NodeId, SourceLoc};
12use crate::utils::push_if_node_or_array;
13
14/// A directly-declared top-level symbol that can be imported.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct TopLevelImportable {
17    /// Symbol name.
18    pub name: String,
19    /// Absolute source path where the symbol is declared.
20    pub declaring_path: String,
21    /// AST node type for this declaration.
22    pub node_type: String,
23    /// LSP completion kind mapped from the AST node type.
24    pub kind: CompletionItemKind,
25}
26
27/// A declaration found within a specific scope.
28#[derive(Debug, Clone)]
29pub struct ScopedDeclaration {
30    /// Variable/function/type name.
31    pub name: String,
32    /// typeIdentifier from typeDescriptions (e.g. "t_struct$_PoolKey_$8887_memory_ptr").
33    pub type_id: String,
34}
35
36/// A byte range identifying a scope-creating AST node.
37#[derive(Debug, Clone)]
38pub struct ScopeRange {
39    /// AST node id of this scope.
40    pub node_id: NodeId,
41    /// Byte offset where this scope starts (from `src` field).
42    pub start: usize,
43    /// Byte offset where this scope ends (start + length).
44    pub end: usize,
45    /// Source file id (from `src` field).
46    pub file_id: FileId,
47}
48
49/// Completion cache built from the AST.
50#[derive(Debug)]
51pub struct CompletionCache {
52    /// All named identifiers as completion items (flat, unscoped).
53    pub names: Vec<CompletionItem>,
54
55    /// name → typeIdentifier (for dot-completion: look up what type a variable is).
56    pub name_to_type: HashMap<String, String>,
57
58    /// node id → Vec<CompletionItem> (members of structs, contracts, enums, libraries).
59    pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
60
61    /// typeIdentifier → node id (resolve a type string to its definition).
62    pub type_to_node: HashMap<String, NodeId>,
63
64    /// contract/library/interface name → node id (for direct name dot-completion like `FullMath.`).
65    pub name_to_node_id: HashMap<String, NodeId>,
66
67    /// node id → Vec<CompletionItem> from methodIdentifiers in .contracts section.
68    /// Full function signatures with 4-byte selectors for contracts/interfaces.
69    pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
70
71    /// (contract_node_id, fn_name) → return typeIdentifier.
72    /// For resolving `foo().` — look up what `foo` returns.
73    pub function_return_types: HashMap<(NodeId, String), String>,
74
75    /// typeIdentifier → Vec<CompletionItem> from UsingForDirective.
76    /// Library functions available on a type via `using X for Y`.
77    pub using_for: HashMap<String, Vec<CompletionItem>>,
78
79    /// Wildcard using-for: `using X for *` — available on all types.
80    pub using_for_wildcard: Vec<CompletionItem>,
81
82    /// Pre-built general completions (AST names + keywords + globals + units).
83    /// Built once, returned by reference on every non-dot completion request.
84    pub general_completions: Vec<CompletionItem>,
85
86    /// scope node_id → declarations in that scope.
87    /// Each scope (Block, FunctionDefinition, ContractDefinition, SourceUnit)
88    /// has the variables/functions/types declared directly within it.
89    pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
90
91    /// node_id → parent scope node_id.
92    /// Walk this chain upward to widen the search scope.
93    pub scope_parent: HashMap<NodeId, NodeId>,
94
95    /// All scope ranges, for finding which scope a byte position falls in.
96    /// Sorted by span size ascending (smallest first) for efficient innermost-scope lookup.
97    pub scope_ranges: Vec<ScopeRange>,
98
99    /// absolute file path → AST source file id.
100    /// Used to map a URI to the file_id needed for scope resolution.
101    pub path_to_file_id: HashMap<String, FileId>,
102
103    /// contract node_id → linearized base contracts (C3 linearization order).
104    /// First element is the contract itself, followed by parents in resolution order.
105    /// Used to search inherited state variables and functions during scope resolution.
106    pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
107
108    /// contract/interface/library node_id → contractKind string.
109    /// Values are `"contract"`, `"interface"`, or `"library"`.
110    /// Used to determine which `type(X).` members to offer.
111    pub contract_kinds: HashMap<NodeId, String>,
112
113    /// Directly-declared importable top-level symbols keyed by symbol name.
114    ///
115    /// This intentionally excludes imported aliases/re-exports and excludes
116    /// non-constant variables. It is used for import-on-completion candidate
117    /// lookup without re-scanning the full AST per request.
118    pub top_level_importables_by_name: HashMap<String, Vec<TopLevelImportable>>,
119
120    /// Directly-declared importable top-level symbols keyed by declaring file path.
121    ///
122    /// This enables cheap incremental invalidation/update on file edits/deletes:
123    /// only the changed file's symbols need to be replaced.
124    pub top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>>,
125}
126
127/// Map AST nodeType to LSP CompletionItemKind.
128fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
129    match node_type {
130        "FunctionDefinition" => CompletionItemKind::FUNCTION,
131        "VariableDeclaration" => CompletionItemKind::VARIABLE,
132        "ContractDefinition" => CompletionItemKind::CLASS,
133        "StructDefinition" => CompletionItemKind::STRUCT,
134        "EnumDefinition" => CompletionItemKind::ENUM,
135        "EnumValue" => CompletionItemKind::ENUM_MEMBER,
136        "EventDefinition" => CompletionItemKind::EVENT,
137        "ErrorDefinition" => CompletionItemKind::EVENT,
138        "ModifierDefinition" => CompletionItemKind::METHOD,
139        "ImportDirective" => CompletionItemKind::MODULE,
140        _ => CompletionItemKind::TEXT,
141    }
142}
143
144/// Parse the `src` field of an AST node: "offset:length:fileId".
145/// Returns the parsed SourceLoc or None if the format is invalid.
146fn parse_src(node: &Value) -> Option<SourceLoc> {
147    let src = node.get("src").and_then(|v| v.as_str())?;
148    SourceLoc::parse(src)
149}
150
151/// Extract the trailing node id from a typeIdentifier string.
152/// e.g. `t_struct$_PoolKey_$8887_storage_ptr` → Some(8887)
153///      `t_contract$_IHooks_$2248` → Some(2248)
154///      `t_uint256` → None
155pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
156    // Pattern: ..._$<digits>... where digits follow the last _$
157    // We find all _$<digits> groups and take the last one that's part of the type name
158    let mut last_id = None;
159    let mut i = 0;
160    let bytes = type_id.as_bytes();
161    while i < bytes.len() {
162        if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
163            i += 2;
164            let start = i;
165            while i < bytes.len() && bytes[i].is_ascii_digit() {
166                i += 1;
167            }
168            if i > start
169                && let Ok(id) = type_id[start..i].parse::<u64>()
170            {
171                last_id = Some(NodeId(id));
172            }
173        } else {
174            i += 1;
175        }
176    }
177    last_id
178}
179
180/// Extract the deepest value type from a mapping typeIdentifier.
181/// Peels off all `t_mapping$_<key>_$_<value>` layers and returns the innermost value type.
182///
183/// e.g. `t_mapping$_t_address_$_t_uint256_$` → `t_uint256`
184///      `t_mapping$_t_address_$_t_mapping$_t_uint256_$_t_uint256_$_$` → `t_uint256`
185///      `t_mapping$_t_userDefinedValueType$_PoolId_$8841_$_t_struct$_State_$4809_storage_$` → `t_struct$_State_$4809_storage`
186pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
187    let mut current = type_id;
188
189    loop {
190        if !current.starts_with("t_mapping$_") {
191            // Not a mapping — this is the value type
192            // Strip trailing _$ suffixes (mapping closers)
193            let result = current.trim_end_matches("_$");
194            return if result.is_empty() {
195                None
196            } else {
197                Some(result.to_string())
198            };
199        }
200
201        // Strip "t_mapping$_" prefix to get "<key>_$_<value>_$"
202        let inner = &current["t_mapping$_".len()..];
203
204        // Find the boundary between key and value.
205        // We need to find the _$_ that separates key from value at depth 0.
206        // Each $_ opens a nesting level, each _$ closes one.
207        let mut depth = 0i32;
208        let bytes = inner.as_bytes();
209        let mut split_pos = None;
210
211        let mut i = 0;
212        while i < bytes.len() {
213            if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
214                depth += 1;
215                i += 2;
216            } else if i + 2 < bytes.len()
217                && bytes[i] == b'_'
218                && bytes[i + 1] == b'$'
219                && bytes[i + 2] == b'_'
220                && depth == 0
221            {
222                // This is the _$_ separator at depth 0
223                split_pos = Some(i);
224                break;
225            } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
226                depth -= 1;
227                i += 2;
228            } else {
229                i += 1;
230            }
231        }
232
233        if let Some(pos) = split_pos {
234            // Value type starts after "_$_"
235            current = &inner[pos + 3..];
236        } else {
237            return None;
238        }
239    }
240}
241
242/// Count parameters in an ABI method signature like "swap((address,address),uint256,bytes)".
243/// Counts commas at depth 0 (inside the outer parens), handling nested tuples.
244fn count_abi_params(signature: &str) -> usize {
245    // Find the first '(' and work from there
246    let start = match signature.find('(') {
247        Some(i) => i + 1,
248        None => return 0,
249    };
250    let bytes = signature.as_bytes();
251    if start >= bytes.len() {
252        return 0;
253    }
254    // Check for empty params "()"
255    if bytes[start] == b')' {
256        return 0;
257    }
258    let mut count = 1; // at least one param if not empty
259    let mut depth = 0;
260    for &b in &bytes[start..] {
261        match b {
262            b'(' => depth += 1,
263            b')' => {
264                if depth == 0 {
265                    break;
266                }
267                depth -= 1;
268            }
269            b',' if depth == 0 => count += 1,
270            _ => {}
271        }
272    }
273    count
274}
275
276/// Count parameters in an AST-derived signature like "swap(PoolKey key, SwapParams params, bytes hookData)".
277fn count_signature_params(sig: &str) -> usize {
278    count_abi_params(sig)
279}
280
281fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
282    match node_type {
283        "ContractDefinition"
284        | "StructDefinition"
285        | "EnumDefinition"
286        | "UserDefinedValueTypeDefinition"
287        | "FunctionDefinition" => true,
288        "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
289        _ => false,
290    }
291}
292
293fn build_top_level_importables_by_name(
294    by_file: &HashMap<String, Vec<TopLevelImportable>>,
295) -> HashMap<String, Vec<TopLevelImportable>> {
296    let mut by_name: HashMap<String, Vec<TopLevelImportable>> = HashMap::new();
297    for symbols in by_file.values() {
298        for symbol in symbols {
299            by_name
300                .entry(symbol.name.clone())
301                .or_default()
302                .push(symbol.clone());
303        }
304    }
305    by_name
306}
307
308/// Extract directly-declared importable top-level symbols from a file AST.
309///
310/// - Includes: contract/interface/library/struct/enum/UDVT/top-level free function/top-level constant
311/// - Excludes: imported aliases/re-exports, nested declarations, non-constant variables
312pub fn extract_top_level_importables_for_file(path: &str, ast: &Value) -> Vec<TopLevelImportable> {
313    let mut out: Vec<TopLevelImportable> = Vec::new();
314    let mut stack: Vec<&Value> = vec![ast];
315    let mut source_unit_id: Option<NodeId> = None;
316
317    while let Some(tree) = stack.pop() {
318        let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
319        let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
320        if node_type == "SourceUnit" {
321            source_unit_id = node_id;
322        }
323        let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
324
325        if !name.is_empty()
326            && is_top_level_importable_decl(node_type, tree)
327            && let Some(src_scope) = source_unit_id
328            && tree.get("scope").and_then(|v| v.as_u64()) == Some(src_scope.0)
329        {
330            out.push(TopLevelImportable {
331                name: name.to_string(),
332                declaring_path: path.to_string(),
333                node_type: node_type.to_string(),
334                kind: node_type_to_completion_kind(node_type),
335            });
336        }
337
338        for key in CHILD_KEYS {
339            push_if_node_or_array(tree, key, &mut stack);
340        }
341    }
342
343    out
344}
345
346impl CompletionCache {
347    /// Replace top-level importables for a file path and rebuild the by-name index.
348    /// Pass an empty `symbols` list when the file is deleted.
349    pub fn replace_top_level_importables_for_path(
350        &mut self,
351        path: String,
352        symbols: Vec<TopLevelImportable>,
353    ) {
354        self.top_level_importables_by_file.insert(path, symbols);
355        self.top_level_importables_by_name =
356            build_top_level_importables_by_name(&self.top_level_importables_by_file);
357    }
358}
359
360/// Build a CompletionCache from AST sources and contracts.
361/// `contracts` is the `.contracts` section of the compiler output (optional).
362pub fn build_completion_cache(sources: &Value, contracts: Option<&Value>) -> CompletionCache {
363    let source_count = sources.as_object().map_or(0, |obj| obj.len());
364    // Pre-size collections based on source count to reduce rehash churn.
365    // Estimates: ~20 names/file, ~5 contracts/file, ~10 functions/file.
366    let est_names = source_count * 20;
367    let est_contracts = source_count * 5;
368
369    let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
370    let mut seen_names: HashMap<String, usize> = HashMap::with_capacity(est_names);
371    let mut name_to_type: HashMap<String, String> = HashMap::with_capacity(est_names);
372    let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
373        HashMap::with_capacity(est_contracts);
374    let mut type_to_node: HashMap<String, NodeId> = HashMap::with_capacity(est_contracts);
375    let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
376        HashMap::with_capacity(est_contracts);
377    let mut name_to_node_id: HashMap<String, NodeId> = HashMap::with_capacity(est_names);
378    let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
379
380    // Collect (path, contract_name, node_id) during AST walk for methodIdentifiers lookup after.
381    let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
382
383    // contract_node_id → fn_name → Vec<signature> (for matching method_identifiers to AST signatures)
384    let mut function_signatures: HashMap<NodeId, HashMap<String, Vec<String>>> =
385        HashMap::with_capacity(est_contracts);
386
387    // (contract_node_id, fn_name) → return typeIdentifier
388    let mut function_return_types: HashMap<(NodeId, String), String> =
389        HashMap::with_capacity(source_count * 10);
390
391    // typeIdentifier → Vec<CompletionItem> from UsingForDirective
392    let mut using_for: HashMap<String, Vec<CompletionItem>> = HashMap::with_capacity(source_count);
393    let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
394
395    // Temp: (library_node_id, target_type_id_or_none) for resolving after walk
396    let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
397
398    // Scope-aware completion data
399    let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
400        HashMap::with_capacity(est_contracts);
401    let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
402    let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
403    let mut path_to_file_id: HashMap<String, FileId> = HashMap::with_capacity(source_count);
404    let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
405        HashMap::with_capacity(est_contracts);
406    let mut top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>> =
407        HashMap::with_capacity(est_names);
408
409    if let Some(sources_obj) = sources.as_object() {
410        for (path, source_data) in sources_obj {
411            if let Some(ast) = source_data.get("ast") {
412                // Map file path → source file id for scope resolution
413                if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
414                    path_to_file_id.insert(path.clone(), FileId(fid));
415                }
416                let file_importables = extract_top_level_importables_for_file(path, ast);
417                if !file_importables.is_empty() {
418                    top_level_importables_by_file.insert(path.clone(), file_importables);
419                }
420                let mut stack: Vec<&Value> = vec![ast];
421
422                while let Some(tree) = stack.pop() {
423                    let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
424                    let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
425                    let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
426
427                    // --- Scope-aware data collection ---
428
429                    // Record scope-creating nodes (SourceUnit, ContractDefinition,
430                    // FunctionDefinition, ModifierDefinition, Block) and their byte ranges.
431                    let is_scope_node = matches!(
432                        node_type,
433                        "SourceUnit"
434                            | "ContractDefinition"
435                            | "FunctionDefinition"
436                            | "ModifierDefinition"
437                            | "Block"
438                            | "UncheckedBlock"
439                    );
440                    if is_scope_node && let Some(nid) = node_id {
441                        if let Some(src_loc) = parse_src(tree) {
442                            scope_ranges.push(ScopeRange {
443                                node_id: nid,
444                                start: src_loc.offset,
445                                end: src_loc.end(),
446                                file_id: src_loc.file_id,
447                            });
448                        }
449                        // Record parent link: this node's scope → its parent
450                        if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_u64()) {
451                            scope_parent.insert(nid, NodeId(parent_id));
452                        }
453                    }
454
455                    // For ContractDefinitions, record linearizedBaseContracts
456                    if node_type == "ContractDefinition"
457                        && let Some(nid) = node_id
458                        && let Some(bases) = tree
459                            .get("linearizedBaseContracts")
460                            .and_then(|v| v.as_array())
461                    {
462                        let base_ids: Vec<NodeId> = bases
463                            .iter()
464                            .filter_map(|b| b.as_u64())
465                            .map(NodeId)
466                            .collect();
467                        if !base_ids.is_empty() {
468                            linearized_base_contracts.insert(nid, base_ids);
469                        }
470                    }
471
472                    // For VariableDeclarations, record the declaration in its scope
473                    if node_type == "VariableDeclaration"
474                        && !name.is_empty()
475                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
476                        && let Some(tid) = tree
477                            .get("typeDescriptions")
478                            .and_then(|td| td.get("typeIdentifier"))
479                            .and_then(|v| v.as_str())
480                    {
481                        scope_declarations
482                            .entry(NodeId(scope_raw))
483                            .or_default()
484                            .push(ScopedDeclaration {
485                                name: name.to_string(),
486                                type_id: tid.to_string(),
487                            });
488                    }
489
490                    // For FunctionDefinitions, record them in their parent scope (the contract)
491                    if node_type == "FunctionDefinition"
492                        && !name.is_empty()
493                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
494                        && let Some(tid) = tree
495                            .get("typeDescriptions")
496                            .and_then(|td| td.get("typeIdentifier"))
497                            .and_then(|v| v.as_str())
498                    {
499                        scope_declarations
500                            .entry(NodeId(scope_raw))
501                            .or_default()
502                            .push(ScopedDeclaration {
503                                name: name.to_string(),
504                                type_id: tid.to_string(),
505                            });
506                    }
507
508                    // Collect named nodes as completion items
509                    if !name.is_empty() && !seen_names.contains_key(name) {
510                        let type_string = tree
511                            .get("typeDescriptions")
512                            .and_then(|td| td.get("typeString"))
513                            .and_then(|v| v.as_str())
514                            .map(|s| s.to_string());
515
516                        let type_id = tree
517                            .get("typeDescriptions")
518                            .and_then(|td| td.get("typeIdentifier"))
519                            .and_then(|v| v.as_str());
520
521                        let kind = node_type_to_completion_kind(node_type);
522
523                        let item = CompletionItem {
524                            label: name.to_string(),
525                            kind: Some(kind),
526                            detail: type_string,
527                            ..Default::default()
528                        };
529
530                        let idx = names.len();
531                        names.push(item);
532                        seen_names.insert(name.to_string(), idx);
533
534                        // Store name → typeIdentifier mapping
535                        if let Some(tid) = type_id {
536                            name_to_type.insert(name.to_string(), tid.to_string());
537                        }
538                    }
539
540                    // Collect struct members
541                    if node_type == "StructDefinition"
542                        && let Some(id) = node_id
543                    {
544                        let mut members = Vec::new();
545                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
546                            for member in member_array {
547                                let member_name =
548                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
549                                if member_name.is_empty() {
550                                    continue;
551                                }
552                                let member_type = member
553                                    .get("typeDescriptions")
554                                    .and_then(|td| td.get("typeString"))
555                                    .and_then(|v| v.as_str())
556                                    .map(|s| s.to_string());
557
558                                members.push(CompletionItem {
559                                    label: member_name.to_string(),
560                                    kind: Some(CompletionItemKind::FIELD),
561                                    detail: member_type,
562                                    ..Default::default()
563                                });
564                            }
565                        }
566                        if !members.is_empty() {
567                            node_members.insert(id, members);
568                        }
569
570                        // Map typeIdentifier → node id
571                        if let Some(tid) = tree
572                            .get("typeDescriptions")
573                            .and_then(|td| td.get("typeIdentifier"))
574                            .and_then(|v| v.as_str())
575                        {
576                            type_to_node.insert(tid.to_string(), id);
577                        }
578                    }
579
580                    // Collect contract/library members (functions, state variables, events, etc.)
581                    if node_type == "ContractDefinition"
582                        && let Some(id) = node_id
583                    {
584                        let mut members = Vec::new();
585                        let mut fn_sigs: HashMap<String, Vec<String>> = HashMap::new();
586                        if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
587                            for member in nodes_array {
588                                let member_type = member
589                                    .get("nodeType")
590                                    .and_then(|v| v.as_str())
591                                    .unwrap_or("");
592                                let member_name =
593                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
594                                if member_name.is_empty() {
595                                    continue;
596                                }
597
598                                // Build function signature and collect return types for FunctionDefinitions
599                                let (member_detail, label_details) =
600                                    if member_type == "FunctionDefinition" {
601                                        // Collect return type for chain resolution.
602                                        // Only single-return functions can be dot-chained
603                                        // (tuples require destructuring).
604                                        if let Some(ret_params) = member
605                                            .get("returnParameters")
606                                            .and_then(|rp| rp.get("parameters"))
607                                            .and_then(|v| v.as_array())
608                                            && ret_params.len() == 1
609                                            && let Some(ret_tid) = ret_params[0]
610                                                .get("typeDescriptions")
611                                                .and_then(|td| td.get("typeIdentifier"))
612                                                .and_then(|v| v.as_str())
613                                        {
614                                            function_return_types.insert(
615                                                (id, member_name.to_string()),
616                                                ret_tid.to_string(),
617                                            );
618                                        }
619
620                                        if let Some(sig) = build_function_signature(member) {
621                                            fn_sigs
622                                                .entry(member_name.to_string())
623                                                .or_default()
624                                                .push(sig.clone());
625                                            (Some(sig), None)
626                                        } else {
627                                            (
628                                                member
629                                                    .get("typeDescriptions")
630                                                    .and_then(|td| td.get("typeString"))
631                                                    .and_then(|v| v.as_str())
632                                                    .map(|s| s.to_string()),
633                                                None,
634                                            )
635                                        }
636                                    } else {
637                                        (
638                                            member
639                                                .get("typeDescriptions")
640                                                .and_then(|td| td.get("typeString"))
641                                                .and_then(|v| v.as_str())
642                                                .map(|s| s.to_string()),
643                                            None,
644                                        )
645                                    };
646
647                                let kind = node_type_to_completion_kind(member_type);
648                                members.push(CompletionItem {
649                                    label: member_name.to_string(),
650                                    kind: Some(kind),
651                                    detail: member_detail,
652                                    label_details,
653                                    ..Default::default()
654                                });
655                            }
656                        }
657                        if !members.is_empty() {
658                            node_members.insert(id, members);
659                        }
660                        if !fn_sigs.is_empty() {
661                            function_signatures.insert(id, fn_sigs);
662                        }
663
664                        if let Some(tid) = tree
665                            .get("typeDescriptions")
666                            .and_then(|td| td.get("typeIdentifier"))
667                            .and_then(|v| v.as_str())
668                        {
669                            type_to_node.insert(tid.to_string(), id);
670                        }
671
672                        // Record for methodIdentifiers lookup after traversal
673                        if !name.is_empty() {
674                            contract_locations.push((path.clone(), name.to_string(), id));
675                            name_to_node_id.insert(name.to_string(), id);
676                        }
677
678                        // Record contractKind (contract, interface, library) for type(X). completions
679                        if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
680                            contract_kinds.insert(id, ck.to_string());
681                        }
682                    }
683
684                    // Collect enum members
685                    if node_type == "EnumDefinition"
686                        && let Some(id) = node_id
687                    {
688                        let mut members = Vec::new();
689                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
690                            for member in member_array {
691                                let member_name =
692                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
693                                if member_name.is_empty() {
694                                    continue;
695                                }
696                                members.push(CompletionItem {
697                                    label: member_name.to_string(),
698                                    kind: Some(CompletionItemKind::ENUM_MEMBER),
699                                    detail: None,
700                                    ..Default::default()
701                                });
702                            }
703                        }
704                        if !members.is_empty() {
705                            node_members.insert(id, members);
706                        }
707
708                        if let Some(tid) = tree
709                            .get("typeDescriptions")
710                            .and_then(|td| td.get("typeIdentifier"))
711                            .and_then(|v| v.as_str())
712                        {
713                            type_to_node.insert(tid.to_string(), id);
714                        }
715                    }
716
717                    // Collect UsingForDirective: using Library for Type
718                    if node_type == "UsingForDirective" {
719                        // Get target type (None = wildcard `for *`)
720                        let target_type = tree.get("typeName").and_then(|tn| {
721                            tn.get("typeDescriptions")
722                                .and_then(|td| td.get("typeIdentifier"))
723                                .and_then(|v| v.as_str())
724                                .map(|s| s.to_string())
725                        });
726
727                        // Form 1: library name object with referencedDeclaration
728                        if let Some(lib) = tree.get("libraryName") {
729                            if let Some(lib_id) =
730                                lib.get("referencedDeclaration").and_then(|v| v.as_u64())
731                            {
732                                using_for_directives.push((NodeId(lib_id), target_type));
733                            }
734                        }
735                        // Form 2: functionList array — individual function references
736                        // These are typically operator overloads (not dot-callable),
737                        // but collect non-operator ones just in case
738                        else if let Some(func_list) =
739                            tree.get("functionList").and_then(|v| v.as_array())
740                        {
741                            for entry in func_list {
742                                // Skip operator overloads
743                                if entry.get("operator").is_some() {
744                                    continue;
745                                }
746                                if let Some(def) = entry.get("definition") {
747                                    let fn_name =
748                                        def.get("name").and_then(|v| v.as_str()).unwrap_or("");
749                                    if !fn_name.is_empty() {
750                                        let items = if let Some(ref tid) = target_type {
751                                            using_for.entry(tid.clone()).or_default()
752                                        } else {
753                                            &mut using_for_wildcard
754                                        };
755                                        items.push(CompletionItem {
756                                            label: fn_name.to_string(),
757                                            kind: Some(CompletionItemKind::FUNCTION),
758                                            detail: None,
759                                            ..Default::default()
760                                        });
761                                    }
762                                }
763                            }
764                        }
765                    }
766
767                    // Traverse children
768                    for key in CHILD_KEYS {
769                        push_if_node_or_array(tree, key, &mut stack);
770                    }
771                }
772            }
773        }
774    }
775
776    // Resolve UsingForDirective library references (Form 1)
777    // Now that node_members is populated, look up each library's functions
778    for (lib_id, target_type) in &using_for_directives {
779        if let Some(lib_members) = node_members.get(lib_id) {
780            let items: Vec<CompletionItem> = lib_members
781                .iter()
782                .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
783                .cloned()
784                .collect();
785            if !items.is_empty() {
786                if let Some(tid) = target_type {
787                    using_for.entry(tid.clone()).or_default().extend(items);
788                } else {
789                    using_for_wildcard.extend(items);
790                }
791            }
792        }
793    }
794
795    // Build method_identifiers from .contracts section
796    if let Some(contracts_val) = contracts
797        && let Some(contracts_obj) = contracts_val.as_object()
798    {
799        for (path, contract_name, node_id) in &contract_locations {
800            // Get AST function signatures for this contract (if available)
801            let fn_sigs = function_signatures.get(node_id);
802
803            if let Some(path_entry) = contracts_obj.get(path)
804                && let Some(contract_entry) = path_entry.get(contract_name)
805                && let Some(evm) = contract_entry.get("evm")
806                && let Some(methods) = evm.get("methodIdentifiers")
807                && let Some(methods_obj) = methods.as_object()
808            {
809                let mut items: Vec<CompletionItem> = Vec::new();
810                for (signature, selector_val) in methods_obj {
811                    // signature is e.g. "swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)"
812                    // selector_val is e.g. "f3cd914c"
813                    let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
814                    let selector_str = selector_val
815                        .as_str()
816                        .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
817                        .unwrap_or_default();
818
819                    // Look up the AST signature with parameter names
820                    let description =
821                        fn_sigs
822                            .and_then(|sigs| sigs.get(&fn_name))
823                            .and_then(|sig_list| {
824                                if sig_list.len() == 1 {
825                                    // Only one overload — use it directly
826                                    Some(sig_list[0].clone())
827                                } else {
828                                    // Multiple overloads — match by parameter count
829                                    let abi_param_count = count_abi_params(signature);
830                                    sig_list
831                                        .iter()
832                                        .find(|s| count_signature_params(s) == abi_param_count)
833                                        .cloned()
834                                }
835                            });
836
837                    items.push(CompletionItem {
838                        label: fn_name,
839                        kind: Some(CompletionItemKind::FUNCTION),
840                        detail: Some(signature.clone()),
841                        label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
842                            detail: Some(selector_str),
843                            description,
844                        }),
845                        ..Default::default()
846                    });
847                }
848                if !items.is_empty() {
849                    method_identifiers.insert(*node_id, items);
850                }
851            }
852        }
853    }
854
855    // Pre-build the general completions list (names + statics) once
856    let mut general_completions = names.clone();
857    general_completions.extend(get_static_completions());
858
859    // Sort scope_ranges by span size ascending (smallest first) for innermost-scope lookup
860    scope_ranges.sort_by_key(|r| r.end - r.start);
861
862    // Infer parent links for Block/UncheckedBlock/ModifierDefinition nodes.
863    // These AST nodes have no `scope` field, so scope_parent has no entry for them.
864    // For each orphan, find the next-larger enclosing scope range in the same file.
865    // scope_ranges is sorted smallest-first, so we scan forward from each orphan's
866    // position to find the first range that strictly contains it.
867    let orphan_ids: Vec<NodeId> = scope_ranges
868        .iter()
869        .filter(|r| !scope_parent.contains_key(&r.node_id))
870        .map(|r| r.node_id)
871        .collect();
872    // Build a lookup from node_id → (start, end, file_id) for quick access
873    let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
874        .iter()
875        .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
876        .collect();
877    for orphan_id in &orphan_ids {
878        if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
879            // Find the smallest range that strictly contains this orphan's range
880            // (same file, starts at or before, ends at or after, and is strictly larger)
881            let parent = scope_ranges
882                .iter()
883                .find(|r| {
884                    r.node_id != *orphan_id
885                        && r.file_id == file_id
886                        && r.start <= start
887                        && r.end >= end
888                        && (r.end - r.start) > (end - start)
889                })
890                .map(|r| r.node_id);
891            if let Some(parent_id) = parent {
892                scope_parent.insert(*orphan_id, parent_id);
893            }
894        }
895    }
896
897    let top_level_importables_by_name =
898        build_top_level_importables_by_name(&top_level_importables_by_file);
899
900    CompletionCache {
901        names,
902        name_to_type,
903        node_members,
904        type_to_node,
905        name_to_node_id,
906        method_identifiers,
907        function_return_types,
908        using_for,
909        using_for_wildcard,
910        general_completions,
911        scope_declarations,
912        scope_parent,
913        scope_ranges,
914        path_to_file_id,
915        linearized_base_contracts,
916        contract_kinds,
917        top_level_importables_by_name,
918        top_level_importables_by_file,
919    }
920}
921
922/// Magic type member definitions (msg, block, tx, abi, address).
923fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
924    let items = match name {
925        "msg" => vec![
926            ("data", "bytes calldata"),
927            ("sender", "address"),
928            ("sig", "bytes4"),
929            ("value", "uint256"),
930        ],
931        "block" => vec![
932            ("basefee", "uint256"),
933            ("blobbasefee", "uint256"),
934            ("chainid", "uint256"),
935            ("coinbase", "address payable"),
936            ("difficulty", "uint256"),
937            ("gaslimit", "uint256"),
938            ("number", "uint256"),
939            ("prevrandao", "uint256"),
940            ("timestamp", "uint256"),
941        ],
942        "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
943        "abi" => vec![
944            ("decode(bytes memory, (...))", "..."),
945            ("encode(...)", "bytes memory"),
946            ("encodePacked(...)", "bytes memory"),
947            ("encodeWithSelector(bytes4, ...)", "bytes memory"),
948            ("encodeWithSignature(string memory, ...)", "bytes memory"),
949            ("encodeCall(function, (...))", "bytes memory"),
950        ],
951        // type(X) — contract type properties
952        // Also includes interface (interfaceId) and integer (min, max) properties
953        "type" => vec![
954            ("name", "string"),
955            ("creationCode", "bytes memory"),
956            ("runtimeCode", "bytes memory"),
957            ("interfaceId", "bytes4"),
958            ("min", "T"),
959            ("max", "T"),
960        ],
961        // bytes and string type-level members
962        "bytes" => vec![("concat(...)", "bytes memory")],
963        "string" => vec![("concat(...)", "string memory")],
964        _ => return None,
965    };
966
967    Some(
968        items
969            .into_iter()
970            .map(|(label, detail)| CompletionItem {
971                label: label.to_string(),
972                kind: Some(CompletionItemKind::PROPERTY),
973                detail: Some(detail.to_string()),
974                ..Default::default()
975            })
976            .collect(),
977    )
978}
979
980/// The kind of argument passed to `type(X)`, which determines which
981/// meta-type members are available.
982#[derive(Debug, Clone, Copy, PartialEq, Eq)]
983enum TypeMetaKind {
984    /// `type(SomeContract)` — has `name`, `creationCode`, `runtimeCode`
985    Contract,
986    /// `type(SomeInterface)` — has `name`, `interfaceId`
987    Interface,
988    /// `type(uint256)` / `type(int8)` — has `min`, `max`
989    IntegerType,
990    /// Unknown argument — return all possible members as a fallback
991    Unknown,
992}
993
994/// Classify the argument of `type(X)` based on the cache.
995fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
996    // Check if it's an integer type: int, uint, int8..int256, uint8..uint256
997    if arg == "int" || arg == "uint" {
998        return TypeMetaKind::IntegerType;
999    }
1000    if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1001        && let Ok(n) = suffix.parse::<u16>()
1002        && (8..=256).contains(&n)
1003        && n % 8 == 0
1004    {
1005        return TypeMetaKind::IntegerType;
1006    }
1007
1008    // With a cache, look up the name to determine contract vs interface
1009    if let Some(c) = cache
1010        && let Some(&node_id) = c.name_to_node_id.get(arg)
1011    {
1012        return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1013            Some("interface") => TypeMetaKind::Interface,
1014            Some("library") => TypeMetaKind::Contract, // libraries have name/creationCode/runtimeCode
1015            _ => TypeMetaKind::Contract,
1016        };
1017    }
1018
1019    TypeMetaKind::Unknown
1020}
1021
1022/// Return context-sensitive `type(X).` completions based on what `X` is.
1023fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1024    let kind = match arg {
1025        Some(a) => classify_type_arg(a, cache),
1026        None => TypeMetaKind::Unknown,
1027    };
1028
1029    let items: Vec<(&str, &str)> = match kind {
1030        TypeMetaKind::Contract => vec![
1031            ("name", "string"),
1032            ("creationCode", "bytes memory"),
1033            ("runtimeCode", "bytes memory"),
1034        ],
1035        TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1036        TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1037        TypeMetaKind::Unknown => vec![
1038            ("name", "string"),
1039            ("creationCode", "bytes memory"),
1040            ("runtimeCode", "bytes memory"),
1041            ("interfaceId", "bytes4"),
1042            ("min", "T"),
1043            ("max", "T"),
1044        ],
1045    };
1046
1047    items
1048        .into_iter()
1049        .map(|(label, detail)| CompletionItem {
1050            label: label.to_string(),
1051            kind: Some(CompletionItemKind::PROPERTY),
1052            detail: Some(detail.to_string()),
1053            ..Default::default()
1054        })
1055        .collect()
1056}
1057
1058/// Address type members (available on any address value).
1059fn address_members() -> Vec<CompletionItem> {
1060    [
1061        ("balance", "uint256", CompletionItemKind::PROPERTY),
1062        ("code", "bytes memory", CompletionItemKind::PROPERTY),
1063        ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1064        ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1065        ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1066        (
1067            "call(bytes memory)",
1068            "(bool, bytes memory)",
1069            CompletionItemKind::FUNCTION,
1070        ),
1071        (
1072            "delegatecall(bytes memory)",
1073            "(bool, bytes memory)",
1074            CompletionItemKind::FUNCTION,
1075        ),
1076        (
1077            "staticcall(bytes memory)",
1078            "(bool, bytes memory)",
1079            CompletionItemKind::FUNCTION,
1080        ),
1081    ]
1082    .iter()
1083    .map(|(label, detail, kind)| CompletionItem {
1084        label: label.to_string(),
1085        kind: Some(*kind),
1086        detail: if detail.is_empty() {
1087            None
1088        } else {
1089            Some(detail.to_string())
1090        },
1091        ..Default::default()
1092    })
1093    .collect()
1094}
1095
1096/// What kind of access precedes the dot.
1097#[derive(Debug, Clone, PartialEq)]
1098pub enum AccessKind {
1099    /// Plain identifier: `foo.`
1100    Plain,
1101    /// Function call: `foo().` or `foo(x, bar()).`
1102    Call,
1103    /// Index/storage access: `foo[key].` or `foo[func()].`
1104    Index,
1105}
1106
1107/// A segment of a dot-expression chain.
1108#[derive(Debug, Clone, PartialEq)]
1109pub struct DotSegment {
1110    pub name: String,
1111    pub kind: AccessKind,
1112    /// For `Call` segments, the raw text inside the parentheses.
1113    /// e.g. `type(uint256).` → `call_args = Some("uint256")`
1114    pub call_args: Option<String>,
1115}
1116
1117/// Skip backwards over a matched bracket pair (parens or square brackets).
1118/// `pos` should point to the closing bracket. Returns the position of the matching
1119/// opening bracket, or 0 if not found.
1120fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1121    let close = bytes[pos];
1122    let open = match close {
1123        b')' => b'(',
1124        b']' => b'[',
1125        _ => return pos,
1126    };
1127    let mut depth = 1u32;
1128    let mut i = pos;
1129    while i > 0 && depth > 0 {
1130        i -= 1;
1131        if bytes[i] == close {
1132            depth += 1;
1133        } else if bytes[i] == open {
1134            depth -= 1;
1135        }
1136    }
1137    i
1138}
1139
1140/// Parse the expression chain before the dot into segments.
1141/// e.g. `poolManager.swap(key, params).` → [("poolManager", Plain), ("swap", Call)]
1142///      `_pools[poolId].fee.` → [("_pools", Index), ("fee", Plain)]
1143///      `msg.` → [("msg", Plain)]
1144pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1145    let col = character as usize;
1146    if col == 0 {
1147        return vec![];
1148    }
1149
1150    let bytes = line.as_bytes();
1151    let mut segments: Vec<DotSegment> = Vec::new();
1152
1153    // Start from the cursor position, skip trailing dot
1154    let mut pos = col;
1155    if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1156        pos -= 1;
1157    }
1158
1159    loop {
1160        if pos == 0 {
1161            break;
1162        }
1163
1164        // Determine access kind by what's immediately before: ')' = Call, ']' = Index, else Plain
1165        let (kind, call_args) = if bytes[pos - 1] == b')' {
1166            let close = pos - 1; // position of ')'
1167            pos = skip_brackets_backwards(bytes, close);
1168            // Extract the text between '(' and ')'
1169            let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1170                .trim()
1171                .to_string();
1172            let args = if args_text.is_empty() {
1173                None
1174            } else {
1175                Some(args_text)
1176            };
1177            (AccessKind::Call, args)
1178        } else if bytes[pos - 1] == b']' {
1179            pos -= 1; // point to ']'
1180            pos = skip_brackets_backwards(bytes, pos);
1181            (AccessKind::Index, None)
1182        } else {
1183            (AccessKind::Plain, None)
1184        };
1185
1186        // Now extract the identifier name (walk backwards over alphanumeric + underscore)
1187        let end = pos;
1188        while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1189            pos -= 1;
1190        }
1191
1192        if pos == end {
1193            // No identifier found (could be something like `().`)
1194            break;
1195        }
1196
1197        let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1198        segments.push(DotSegment {
1199            name,
1200            kind,
1201            call_args,
1202        });
1203
1204        // Check if there's a dot before this segment (meaning more chain)
1205        if pos > 0 && bytes[pos - 1] == b'.' {
1206            pos -= 1; // skip the dot, continue parsing next segment
1207        } else {
1208            break;
1209        }
1210    }
1211
1212    segments.reverse(); // We parsed right-to-left, flip to left-to-right
1213    segments
1214}
1215
1216/// Extract the identifier before the cursor (the word before the dot).
1217/// Returns just the last identifier name for backward compatibility.
1218pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1219    let segments = parse_dot_chain(line, character);
1220    segments.last().map(|s| s.name.clone())
1221}
1222
1223#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1224Solidity AST uses different suffixes in different contexts:
1225  - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1226  - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1227  - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1228All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1229fn strip_type_suffix(type_id: &str) -> &str {
1230    let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1231    s.strip_suffix("_storage")
1232        .or_else(|| s.strip_suffix("_memory"))
1233        .or_else(|| s.strip_suffix("_calldata"))
1234        .unwrap_or(s)
1235}
1236
1237/// Look up using-for completions for a type, trying suffix variants.
1238/// The AST stores types with different suffixes (_storage_ptr, _storage, _memory_ptr, etc.)
1239/// across different contexts, so we try multiple forms.
1240fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1241    // Exact match first
1242    if let Some(items) = cache.using_for.get(type_id) {
1243        return items.clone();
1244    }
1245
1246    // Strip to base form, then try all common suffix variants
1247    let base = strip_type_suffix(type_id);
1248    let variants = [
1249        base.to_string(),
1250        format!("{}_storage", base),
1251        format!("{}_storage_ptr", base),
1252        format!("{}_memory", base),
1253        format!("{}_memory_ptr", base),
1254        format!("{}_calldata", base),
1255    ];
1256    for variant in &variants {
1257        if variant.as_str() != type_id
1258            && let Some(items) = cache.using_for.get(variant.as_str())
1259        {
1260            return items.clone();
1261        }
1262    }
1263
1264    vec![]
1265}
1266
1267/// Collect completions available for a given typeIdentifier.
1268/// Includes node_members, method_identifiers, using_for, and using_for_wildcard.
1269fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1270    // Address type
1271    if type_id == "t_address" || type_id == "t_address_payable" {
1272        let mut items = address_members();
1273        // Also add using-for on address
1274        if let Some(uf) = cache.using_for.get(type_id) {
1275            items.extend(uf.iter().cloned());
1276        }
1277        items.extend(cache.using_for_wildcard.iter().cloned());
1278        return items;
1279    }
1280
1281    let resolved_node_id = extract_node_id_from_type(type_id)
1282        .or_else(|| cache.type_to_node.get(type_id).copied())
1283        .or_else(|| {
1284            // Handle synthetic __node_id_ markers from name_to_node_id fallback
1285            type_id
1286                .strip_prefix("__node_id_")
1287                .and_then(|s| s.parse::<u64>().ok())
1288                .map(NodeId)
1289        });
1290
1291    let mut items = Vec::new();
1292    let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1293
1294    if let Some(node_id) = resolved_node_id {
1295        // Method identifiers first — they have full signatures with selectors
1296        if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1297            for item in method_items {
1298                seen_labels.insert(item.label.clone());
1299                items.push(item.clone());
1300            }
1301        }
1302
1303        // Supplement with node_members (state variables, events, errors, modifiers, etc.)
1304        if let Some(members) = cache.node_members.get(&node_id) {
1305            for item in members {
1306                if !seen_labels.contains(&item.label) {
1307                    seen_labels.insert(item.label.clone());
1308                    items.push(item.clone());
1309                }
1310            }
1311        }
1312    }
1313
1314    // Add using-for library functions, but only for value types — not for
1315    // contract/library/interface names. When you type `Lock.`, you want Lock's
1316    // own members, not functions from `using Pool for *` or `using SafeCast for *`.
1317    let is_contract_name = resolved_node_id
1318        .map(|nid| cache.contract_kinds.contains_key(&nid))
1319        .unwrap_or(false);
1320
1321    if !is_contract_name {
1322        // Try exact match first, then try normalized variants (storage_ptr vs storage vs memory_ptr etc.)
1323        let uf_items = lookup_using_for(cache, type_id);
1324        for item in &uf_items {
1325            if !seen_labels.contains(&item.label) {
1326                seen_labels.insert(item.label.clone());
1327                items.push(item.clone());
1328            }
1329        }
1330
1331        // Add wildcard using-for (using X for *)
1332        for item in &cache.using_for_wildcard {
1333            if !seen_labels.contains(&item.label) {
1334                seen_labels.insert(item.label.clone());
1335                items.push(item.clone());
1336            }
1337        }
1338    }
1339
1340    items
1341}
1342
1343/// Resolve a type identifier for a name, considering name_to_type and name_to_node_id.
1344fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1345    // Direct type lookup
1346    if let Some(tid) = cache.name_to_type.get(name) {
1347        return Some(tid.clone());
1348    }
1349    // Contract/library/interface name → synthesize a type id from node id
1350    if let Some(node_id) = cache.name_to_node_id.get(name) {
1351        // Find a matching typeIdentifier in type_to_node (reverse lookup)
1352        for (tid, nid) in &cache.type_to_node {
1353            if nid == node_id {
1354                return Some(tid.clone());
1355            }
1356        }
1357        // Fallback: use a synthetic marker so completions_for_type can resolve via node_id
1358        return Some(format!("__node_id_{}", node_id));
1359    }
1360    None
1361}
1362
1363/// Find the innermost scope node that contains the given byte position and file.
1364/// `scope_ranges` must be sorted by span size ascending (smallest first).
1365/// Returns the node_id of the smallest scope enclosing the position.
1366pub fn find_innermost_scope(
1367    cache: &CompletionCache,
1368    byte_pos: usize,
1369    file_id: FileId,
1370) -> Option<NodeId> {
1371    // scope_ranges is sorted smallest-first, so the first match is the innermost scope
1372    cache
1373        .scope_ranges
1374        .iter()
1375        .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1376        .map(|r| r.node_id)
1377}
1378
1379/// Resolve a variable name to its type by walking up the scope chain.
1380///
1381/// Starting from the innermost scope at the cursor position, check each scope's
1382/// declarations for a matching name. If not found, follow `scope_parent` to the
1383/// next enclosing scope and check again. Stop at the first match.
1384///
1385/// Falls back to `resolve_name_to_type_id` (flat lookup) if scope resolution
1386/// finds nothing, or if the scope data is unavailable.
1387pub fn resolve_name_in_scope(
1388    cache: &CompletionCache,
1389    name: &str,
1390    byte_pos: usize,
1391    file_id: FileId,
1392) -> Option<String> {
1393    let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1394
1395    // Walk up the scope chain
1396    loop {
1397        // Check declarations in this scope
1398        if let Some(decls) = cache.scope_declarations.get(&current_scope) {
1399            for decl in decls {
1400                if decl.name == name {
1401                    return Some(decl.type_id.clone());
1402                }
1403            }
1404        }
1405
1406        // If this scope is a contract, also search inherited contracts
1407        // in C3 linearization order (skipping index 0 which is the contract itself,
1408        // since we already checked its declarations above).
1409        if let Some(bases) = cache.linearized_base_contracts.get(&current_scope) {
1410            for &base_id in bases.iter().skip(1) {
1411                if let Some(decls) = cache.scope_declarations.get(&base_id) {
1412                    for decl in decls {
1413                        if decl.name == name {
1414                            return Some(decl.type_id.clone());
1415                        }
1416                    }
1417                }
1418            }
1419        }
1420
1421        // Move up to parent scope
1422        match cache.scope_parent.get(&current_scope) {
1423            Some(&parent_id) => current_scope = parent_id,
1424            None => break, // reached the top (SourceUnit has no parent)
1425        }
1426    }
1427
1428    // Scope walk found nothing — fall back to flat lookup
1429    // (handles contract/library names which aren't in scope_declarations)
1430    resolve_name_to_type_id(cache, name)
1431}
1432
1433/// Resolve a name within a type context to get the member's type.
1434/// `context_type_id` is the type of the object before the dot.
1435/// `member_name` is the name after the dot.
1436/// `kind` determines how to interpret the result (Call = return type, Index = mapping value, Plain = member type).
1437fn resolve_member_type(
1438    cache: &CompletionCache,
1439    context_type_id: &str,
1440    member_name: &str,
1441    kind: &AccessKind,
1442) -> Option<String> {
1443    let resolved_node_id = extract_node_id_from_type(context_type_id)
1444        .or_else(|| cache.type_to_node.get(context_type_id).copied())
1445        .or_else(|| {
1446            // Handle synthetic __node_id_ markers
1447            context_type_id
1448                .strip_prefix("__node_id_")
1449                .and_then(|s| s.parse::<u64>().ok())
1450                .map(NodeId)
1451        });
1452
1453    let node_id = resolved_node_id?;
1454
1455    match kind {
1456        AccessKind::Call => {
1457            // Look up the function's return type
1458            cache
1459                .function_return_types
1460                .get(&(node_id, member_name.to_string()))
1461                .cloned()
1462        }
1463        AccessKind::Index => {
1464            // Look up the member's type, then extract mapping value type
1465            if let Some(members) = cache.node_members.get(&node_id) {
1466                for member in members {
1467                    if member.label == member_name {
1468                        // Get the typeIdentifier from name_to_type
1469                        if let Some(tid) = cache.name_to_type.get(member_name) {
1470                            if tid.starts_with("t_mapping") {
1471                                return extract_mapping_value_type(tid);
1472                            }
1473                            return Some(tid.clone());
1474                        }
1475                    }
1476                }
1477            }
1478            // Also check: the identifier itself might be a mapping variable
1479            if let Some(tid) = cache.name_to_type.get(member_name)
1480                && tid.starts_with("t_mapping")
1481            {
1482                return extract_mapping_value_type(tid);
1483            }
1484            None
1485        }
1486        AccessKind::Plain => {
1487            // Look up member's own type from name_to_type
1488            cache.name_to_type.get(member_name).cloned()
1489        }
1490    }
1491}
1492
1493/// Scope context for scope-aware completion resolution.
1494/// When present, type resolution uses the scope chain at the cursor position
1495/// instead of the flat first-wins `name_to_type` map.
1496pub struct ScopeContext {
1497    /// Byte offset of the cursor in the source file.
1498    pub byte_pos: usize,
1499    /// Source file id (from the AST `src` field).
1500    pub file_id: FileId,
1501}
1502
1503/// Resolve a name to a type, using scope context if available.
1504/// With scope context: walks up the scope chain from the cursor position.
1505/// Without: falls back to flat `name_to_type` lookup.
1506fn resolve_name(
1507    cache: &CompletionCache,
1508    name: &str,
1509    scope_ctx: Option<&ScopeContext>,
1510) -> Option<String> {
1511    if let Some(ctx) = scope_ctx {
1512        resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1513    } else {
1514        resolve_name_to_type_id(cache, name)
1515    }
1516}
1517
1518/// Get completions for a dot-completion request by resolving the full expression chain.
1519pub fn get_dot_completions(
1520    cache: &CompletionCache,
1521    identifier: &str,
1522    scope_ctx: Option<&ScopeContext>,
1523) -> Vec<CompletionItem> {
1524    // Simple single-segment case (backward compat) — just use the identifier directly
1525    if let Some(items) = magic_members(identifier) {
1526        return items;
1527    }
1528
1529    // Try to resolve the identifier's type
1530    let type_id = resolve_name(cache, identifier, scope_ctx);
1531
1532    if let Some(tid) = type_id {
1533        return completions_for_type(cache, &tid);
1534    }
1535
1536    vec![]
1537}
1538
1539/// Get completions by resolving a full dot-expression chain.
1540/// This is the main entry point for dot-completions with chaining support.
1541pub fn get_chain_completions(
1542    cache: &CompletionCache,
1543    chain: &[DotSegment],
1544    scope_ctx: Option<&ScopeContext>,
1545) -> Vec<CompletionItem> {
1546    if chain.is_empty() {
1547        return vec![];
1548    }
1549
1550    // Single segment: simple dot-completion
1551    if chain.len() == 1 {
1552        let seg = &chain[0];
1553
1554        // For Call/Index on the single segment, we need to resolve the return/value type
1555        match seg.kind {
1556            AccessKind::Plain => {
1557                return get_dot_completions(cache, &seg.name, scope_ctx);
1558            }
1559            AccessKind::Call => {
1560                // type(X). — Solidity metatype expression
1561                if seg.name == "type" {
1562                    return type_meta_members(seg.call_args.as_deref(), Some(cache));
1563                }
1564                // foo(). — could be a function call or a type cast like IFoo(addr).
1565                // First check if it's a type cast: name matches a contract/interface/library
1566                if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1567                    return completions_for_type(cache, &type_id);
1568                }
1569                // Otherwise look up as a function call — check all function_return_types
1570                for ((_, fn_name), ret_type) in &cache.function_return_types {
1571                    if fn_name == &seg.name {
1572                        return completions_for_type(cache, ret_type);
1573                    }
1574                }
1575                return vec![];
1576            }
1577            AccessKind::Index => {
1578                // foo[key]. — look up foo's type and extract mapping value type
1579                if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1580                    && tid.starts_with("t_mapping")
1581                    && let Some(val_type) = extract_mapping_value_type(&tid)
1582                {
1583                    return completions_for_type(cache, &val_type);
1584                }
1585                return vec![];
1586            }
1587        }
1588    }
1589
1590    // Multi-segment chain: resolve step by step
1591    // First segment: resolve to a type (scope-aware when available)
1592    let first = &chain[0];
1593    let mut current_type = match first.kind {
1594        AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1595        AccessKind::Call => {
1596            // Type cast (e.g. IFoo(addr).) or free function call at the start
1597            resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1598                cache
1599                    .function_return_types
1600                    .iter()
1601                    .find(|((_, fn_name), _)| fn_name == &first.name)
1602                    .map(|(_, ret_type)| ret_type.clone())
1603            })
1604        }
1605        AccessKind::Index => {
1606            // Mapping access at the start
1607            resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1608                if tid.starts_with("t_mapping") {
1609                    extract_mapping_value_type(&tid)
1610                } else {
1611                    Some(tid)
1612                }
1613            })
1614        }
1615    };
1616
1617    // Middle segments: resolve each to advance the type
1618    for seg in &chain[1..] {
1619        let ctx_type = match &current_type {
1620            Some(t) => t.clone(),
1621            None => return vec![],
1622        };
1623
1624        current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1625    }
1626
1627    // Return completions for the final resolved type
1628    match current_type {
1629        Some(tid) => completions_for_type(cache, &tid),
1630        None => vec![],
1631    }
1632}
1633
1634/// Get static completions that never change (keywords, magic globals, global functions, units).
1635/// These are available immediately without an AST cache.
1636pub fn get_static_completions() -> Vec<CompletionItem> {
1637    let mut items = Vec::new();
1638
1639    // Add Solidity keywords
1640    for kw in SOLIDITY_KEYWORDS {
1641        items.push(CompletionItem {
1642            label: kw.to_string(),
1643            kind: Some(CompletionItemKind::KEYWORD),
1644            ..Default::default()
1645        });
1646    }
1647
1648    // Add magic globals
1649    for (name, detail) in MAGIC_GLOBALS {
1650        items.push(CompletionItem {
1651            label: name.to_string(),
1652            kind: Some(CompletionItemKind::VARIABLE),
1653            detail: Some(detail.to_string()),
1654            ..Default::default()
1655        });
1656    }
1657
1658    // Add global functions
1659    for (name, detail) in GLOBAL_FUNCTIONS {
1660        items.push(CompletionItem {
1661            label: name.to_string(),
1662            kind: Some(CompletionItemKind::FUNCTION),
1663            detail: Some(detail.to_string()),
1664            ..Default::default()
1665        });
1666    }
1667
1668    // Add ether denomination units
1669    for (name, detail) in ETHER_UNITS {
1670        items.push(CompletionItem {
1671            label: name.to_string(),
1672            kind: Some(CompletionItemKind::UNIT),
1673            detail: Some(detail.to_string()),
1674            ..Default::default()
1675        });
1676    }
1677
1678    // Add time units
1679    for (name, detail) in TIME_UNITS {
1680        items.push(CompletionItem {
1681            label: name.to_string(),
1682            kind: Some(CompletionItemKind::UNIT),
1683            detail: Some(detail.to_string()),
1684            ..Default::default()
1685        });
1686    }
1687
1688    items
1689}
1690
1691/// Get general completions (all known names + static completions).
1692pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1693    let mut items = cache.names.clone();
1694    items.extend(get_static_completions());
1695    items
1696}
1697
1698/// Append auto-import candidates at the tail of completion results.
1699///
1700/// This enforces lower priority ordering by:
1701/// 1) appending after base completions
1702/// 2) assigning high `sortText` values (`zz_autoimport_*`) when missing
1703pub fn append_auto_import_candidates_last(
1704    mut base: Vec<CompletionItem>,
1705    mut auto_import_candidates: Vec<CompletionItem>,
1706) -> Vec<CompletionItem> {
1707    let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1708    for item in &auto_import_candidates {
1709        let entry = unique_label_edits
1710            .entry(item.label.clone())
1711            .or_insert_with(|| item.additional_text_edits.clone());
1712        if *entry != item.additional_text_edits {
1713            *entry = None;
1714        }
1715    }
1716
1717    // If a label maps to exactly one import edit, attach it to the corresponding
1718    // base completion item too. This ensures accepting the normal item can still
1719    // apply import edits in clients that de-prioritize or collapse duplicate labels.
1720    for item in &mut base {
1721        if item.additional_text_edits.is_none()
1722            && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1723        {
1724            item.additional_text_edits = Some(edits.clone());
1725        }
1726    }
1727
1728    for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1729        if item.sort_text.is_none() {
1730            item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1731        }
1732    }
1733
1734    base.extend(auto_import_candidates);
1735    base
1736}
1737
1738/// Convert cached top-level importable symbols into completion items.
1739///
1740/// These are intended as low-priority tail candidates appended after normal
1741/// per-file completions.
1742pub fn top_level_importable_completion_candidates(
1743    cache: &CompletionCache,
1744    current_file_path: Option<&str>,
1745    source_text: &str,
1746) -> Vec<CompletionItem> {
1747    let mut out = Vec::new();
1748    for symbols in cache.top_level_importables_by_name.values() {
1749        for symbol in symbols {
1750            if let Some(cur) = current_file_path
1751                && cur == symbol.declaring_path
1752            {
1753                continue;
1754            }
1755
1756            let import_path = match current_file_path.and_then(|cur| {
1757                to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
1758            }) {
1759                Some(p) => p,
1760                None => continue,
1761            };
1762
1763            if import_statement_already_present(source_text, &symbol.name, &import_path) {
1764                continue;
1765            }
1766
1767            let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
1768            out.push(CompletionItem {
1769                label: symbol.name.clone(),
1770                kind: Some(symbol.kind),
1771                detail: Some(format!("{} ({import_path})", symbol.node_type)),
1772                additional_text_edits: import_edit.map(|e| vec![e]),
1773                ..Default::default()
1774            });
1775        }
1776    }
1777    out
1778}
1779
1780fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
1781    let from_dir = current_file.parent()?;
1782    let rel = pathdiff::diff_paths(target_file, from_dir)?;
1783    let mut s = rel.to_string_lossy().replace('\\', "/");
1784    if !s.starts_with("./") && !s.starts_with("../") {
1785        s = format!("./{s}");
1786    }
1787    Some(s)
1788}
1789
1790fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
1791    let named = format!("import {{{symbol}}} from \"{import_path}\";");
1792    let full = format!("import \"{import_path}\";");
1793    source_text.contains(&named) || source_text.contains(&full)
1794}
1795
1796fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
1797    let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
1798    let lines: Vec<&str> = source_text.lines().collect();
1799
1800    let last_import_line = lines
1801        .iter()
1802        .enumerate()
1803        .filter(|(_, line)| line.trim_start().starts_with("import "))
1804        .map(|(idx, _)| idx)
1805        .last();
1806
1807    let insert_line = if let Some(idx) = last_import_line {
1808        idx + 1
1809    } else if let Some(idx) = lines
1810        .iter()
1811        .enumerate()
1812        .filter(|(_, line)| line.trim_start().starts_with("pragma "))
1813        .map(|(idx, _)| idx)
1814        .last()
1815    {
1816        idx + 1
1817    } else {
1818        0
1819    };
1820
1821    Some(TextEdit {
1822        range: Range {
1823            start: Position {
1824                line: insert_line as u32,
1825                character: 0,
1826            },
1827            end: Position {
1828                line: insert_line as u32,
1829                character: 0,
1830            },
1831        },
1832        new_text: import_stmt,
1833    })
1834}
1835
1836/// Handle a completion request with optional tail candidates.
1837///
1838/// Tail candidates are only appended for non-dot completions and are always
1839/// ordered last via `append_auto_import_candidates_last`.
1840pub fn handle_completion_with_tail_candidates(
1841    cache: Option<&CompletionCache>,
1842    source_text: &str,
1843    position: Position,
1844    trigger_char: Option<&str>,
1845    file_id: Option<FileId>,
1846    tail_candidates: Vec<CompletionItem>,
1847) -> Option<CompletionResponse> {
1848    let lines: Vec<&str> = source_text.lines().collect();
1849    let line = lines.get(position.line as usize)?;
1850
1851    // Convert encoding-aware column to a byte offset within this line.
1852    let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
1853    let line_start_byte: usize = source_text[..abs_byte]
1854        .rfind('\n')
1855        .map(|i| i + 1)
1856        .unwrap_or(0);
1857    let col_byte = (abs_byte - line_start_byte) as u32;
1858
1859    // Build scope context for scope-aware type resolution
1860    let scope_ctx = file_id.map(|fid| ScopeContext {
1861        byte_pos: abs_byte,
1862        file_id: fid,
1863    });
1864
1865    let items = if trigger_char == Some(".") {
1866        let chain = parse_dot_chain(line, col_byte);
1867        if chain.is_empty() {
1868            return None;
1869        }
1870        match cache {
1871            Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
1872            None => {
1873                // No cache yet — serve magic dot completions (msg., block., etc.)
1874                if chain.len() == 1 {
1875                    let seg = &chain[0];
1876                    if seg.name == "type" && seg.kind == AccessKind::Call {
1877                        // type(X). without cache — classify based on name alone
1878                        type_meta_members(seg.call_args.as_deref(), None)
1879                    } else if seg.kind == AccessKind::Plain {
1880                        magic_members(&seg.name).unwrap_or_default()
1881                    } else {
1882                        vec![]
1883                    }
1884                } else {
1885                    vec![]
1886                }
1887            }
1888        }
1889    } else {
1890        match cache {
1891            Some(c) => {
1892                append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
1893            }
1894            None => get_static_completions(),
1895        }
1896    };
1897
1898    Some(CompletionResponse::List(CompletionList {
1899        is_incomplete: cache.is_none(),
1900        items,
1901    }))
1902}
1903
1904/// Handle a completion request.
1905///
1906/// When `cache` is `Some`, full AST-aware completions are returned.
1907/// When `cache` is `None`, only static completions (keywords, globals, units)
1908/// and magic dot completions (msg., block., tx., abi., type().) are returned
1909/// immediately — no blocking.
1910///
1911/// `file_id` is the AST source file id, needed for scope-aware resolution.
1912/// When `None`, scope resolution is skipped and flat lookup is used.
1913pub fn handle_completion(
1914    cache: Option<&CompletionCache>,
1915    source_text: &str,
1916    position: Position,
1917    trigger_char: Option<&str>,
1918    file_id: Option<FileId>,
1919) -> Option<CompletionResponse> {
1920    handle_completion_with_tail_candidates(
1921        cache,
1922        source_text,
1923        position,
1924        trigger_char,
1925        file_id,
1926        vec![],
1927    )
1928}
1929
1930const SOLIDITY_KEYWORDS: &[&str] = &[
1931    "abstract",
1932    "address",
1933    "assembly",
1934    "bool",
1935    "break",
1936    "bytes",
1937    "bytes1",
1938    "bytes4",
1939    "bytes32",
1940    "calldata",
1941    "constant",
1942    "constructor",
1943    "continue",
1944    "contract",
1945    "delete",
1946    "do",
1947    "else",
1948    "emit",
1949    "enum",
1950    "error",
1951    "event",
1952    "external",
1953    "fallback",
1954    "false",
1955    "for",
1956    "function",
1957    "if",
1958    "immutable",
1959    "import",
1960    "indexed",
1961    "int8",
1962    "int24",
1963    "int128",
1964    "int256",
1965    "interface",
1966    "internal",
1967    "library",
1968    "mapping",
1969    "memory",
1970    "modifier",
1971    "new",
1972    "override",
1973    "payable",
1974    "pragma",
1975    "private",
1976    "public",
1977    "pure",
1978    "receive",
1979    "return",
1980    "returns",
1981    "revert",
1982    "storage",
1983    "string",
1984    "struct",
1985    "true",
1986    "type",
1987    "uint8",
1988    "uint24",
1989    "uint128",
1990    "uint160",
1991    "uint256",
1992    "unchecked",
1993    "using",
1994    "view",
1995    "virtual",
1996    "while",
1997];
1998
1999/// Ether denomination units — suffixes for literal numbers.
2000const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2001
2002/// Time units — suffixes for literal numbers.
2003const TIME_UNITS: &[(&str, &str)] = &[
2004    ("seconds", "1"),
2005    ("minutes", "60 seconds"),
2006    ("hours", "3600 seconds"),
2007    ("days", "86400 seconds"),
2008    ("weeks", "604800 seconds"),
2009];
2010
2011const MAGIC_GLOBALS: &[(&str, &str)] = &[
2012    ("msg", "msg"),
2013    ("block", "block"),
2014    ("tx", "tx"),
2015    ("abi", "abi"),
2016    ("this", "address"),
2017    ("super", "contract"),
2018    ("type", "type information"),
2019];
2020
2021const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2022    // Mathematical and Cryptographic Functions
2023    ("addmod(uint256, uint256, uint256)", "uint256"),
2024    ("mulmod(uint256, uint256, uint256)", "uint256"),
2025    ("keccak256(bytes memory)", "bytes32"),
2026    ("sha256(bytes memory)", "bytes32"),
2027    ("ripemd160(bytes memory)", "bytes20"),
2028    (
2029        "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2030        "address",
2031    ),
2032    // Block and Transaction Properties (functions)
2033    ("blockhash(uint256 blockNumber)", "bytes32"),
2034    ("blobhash(uint256 index)", "bytes32"),
2035    ("gasleft()", "uint256"),
2036    // Error Handling
2037    ("assert(bool condition)", ""),
2038    ("require(bool condition)", ""),
2039    ("require(bool condition, string memory message)", ""),
2040    ("revert()", ""),
2041    ("revert(string memory reason)", ""),
2042    // Contract-related
2043    ("selfdestruct(address payable recipient)", ""),
2044];
2045
2046// ---------------------------------------------------------------------------
2047// Import path completions
2048// ---------------------------------------------------------------------------
2049
2050/// Walk `project_root` recursively and return every `.sol` file as:
2051///   1. A relative path from the current file's directory (e.g. `./libraries/Pool.sol`)
2052///   2. A remapped path for each matching remapping (e.g. `forge-std/Test.sol`)
2053///
2054/// Skips `out/`, `cache/`, `node_modules/`, `.git/`.
2055///
2056/// `typed_range` is `Some((line, start_col, end_col))` where `start_col` is the
2057/// character column right after the opening quote and `end_col` is the cursor
2058/// column. When provided, each item gets a `text_edit` that replaces the
2059/// already-typed text so the insert doesn't double-up, plus `filter_text = label`
2060/// so clients filter on the full path rather than the word at cursor.
2061pub fn all_sol_import_paths(
2062    current_file: &Path,
2063    project_root: &Path,
2064    remappings: &[String],
2065    typed_range: Option<(u32, u32, u32)>,
2066) -> Vec<CompletionItem> {
2067    let current_dir = match current_file.parent() {
2068        Some(d) => d,
2069        None => return vec![],
2070    };
2071
2072    // Pre-parse remappings into (prefix, abs_target_path) pairs.
2073    // e.g. "forge-std/=lib/forge-std/src/" → ("forge-std/", "/project/lib/forge-std/src/")
2074    // Canonicalize the target so strip_prefix works even when the path contains
2075    // `..` segments (e.g. lib/forge-std/../src/ would otherwise produce bad labels).
2076    let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2077        .iter()
2078        .filter_map(|r| {
2079            let mut it = r.splitn(2, '=');
2080            let prefix = it.next()?.to_string();
2081            let target = it.next()?;
2082            if prefix.is_empty() || target.is_empty() {
2083                return None;
2084            }
2085            let raw = project_root.join(target);
2086            let canonical = raw.canonicalize().unwrap_or(raw);
2087            Some((prefix, canonical))
2088        })
2089        .collect();
2090
2091    let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2092    let mut items = Vec::new();
2093
2094    collect_sol_files(
2095        project_root,
2096        current_dir,
2097        &parsed_remappings,
2098        skip_dirs,
2099        typed_range,
2100        &mut items,
2101    );
2102    items.sort_by(|a, b| a.label.cmp(&b.label));
2103    items
2104}
2105
2106fn collect_sol_files(
2107    dir: &Path,
2108    current_dir: &Path,
2109    remappings: &[(String, std::path::PathBuf)],
2110    skip_dirs: &[&str],
2111    typed_range: Option<(u32, u32, u32)>,
2112    out: &mut Vec<CompletionItem>,
2113) {
2114    let entries = match std::fs::read_dir(dir) {
2115        Ok(e) => e,
2116        Err(_) => return,
2117    };
2118    for entry in entries.flatten() {
2119        let path = entry.path();
2120        let name = entry.file_name();
2121        let name_str = name.to_string_lossy();
2122
2123        if name_str.starts_with('.') {
2124            continue;
2125        }
2126
2127        if path.is_dir() {
2128            if skip_dirs.contains(&name_str.as_ref()) {
2129                continue;
2130            }
2131            collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2132            continue;
2133        }
2134
2135        if !path.is_file() || !name_str.ends_with(".sol") {
2136            continue;
2137        }
2138
2139        // 1. Relative path (always emitted)
2140        if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2141            let s = rel.to_string_lossy().to_string();
2142            let label = if s.starts_with("../") || s.starts_with("./") {
2143                s
2144            } else {
2145                format!("./{s}")
2146            };
2147            out.push(make_import_item(label, typed_range));
2148        }
2149
2150        // 2. Remapped paths — for every remapping whose target is a prefix of
2151        //    this file's absolute path, emit the remapped form.
2152        //    e.g. file=/project/lib/forge-std/src/Test.sol
2153        //         remap forge-std/=/project/lib/forge-std/src/
2154        //         → label = "forge-std/Test.sol"
2155        for (prefix, target_abs) in remappings {
2156            if let Ok(suffix) = path.strip_prefix(target_abs) {
2157                let suffix_str = suffix
2158                    .to_string_lossy()
2159                    .trim_start_matches(['/', '\\'])
2160                    .to_string();
2161                let label = format!("{}{}", prefix, suffix_str);
2162                out.push(make_import_item(label, typed_range));
2163            }
2164        }
2165    }
2166}
2167
2168fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2169    // filter_text = label so that clients whose word-at-cursor is shorter than
2170    // the full path (e.g. just "Pool" when the label is "./src/Pool.sol") still
2171    // match correctly via substring/fuzzy matching against the full path.
2172    //
2173    // text_edit replaces the already-typed text inside the quotes so the insert
2174    // doesn't append after what was already typed.
2175    let text_edit = typed_range.map(|(line, start_col, end_col)| {
2176        tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2177            range: Range {
2178                start: Position {
2179                    line,
2180                    character: start_col,
2181                },
2182                end: Position {
2183                    line,
2184                    character: end_col,
2185                },
2186            },
2187            new_text: label.clone(),
2188        })
2189    });
2190    CompletionItem {
2191        label: label.clone(),
2192        filter_text: Some(label.clone()),
2193        text_edit,
2194        kind: Some(CompletionItemKind::FILE),
2195        ..Default::default()
2196    }
2197}
2198
2199#[cfg(test)]
2200mod tests {
2201    use super::{
2202        CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2203        build_completion_cache, extract_top_level_importables_for_file,
2204    };
2205    use serde_json::json;
2206    use std::collections::HashMap;
2207    use tower_lsp::lsp_types::CompletionItemKind;
2208    use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2209
2210    fn empty_cache() -> CompletionCache {
2211        CompletionCache {
2212            names: vec![],
2213            name_to_type: HashMap::new(),
2214            node_members: HashMap::new(),
2215            type_to_node: HashMap::new(),
2216            name_to_node_id: HashMap::new(),
2217            method_identifiers: HashMap::new(),
2218            function_return_types: HashMap::new(),
2219            using_for: HashMap::new(),
2220            using_for_wildcard: vec![],
2221            general_completions: vec![],
2222            scope_declarations: HashMap::new(),
2223            scope_parent: HashMap::new(),
2224            scope_ranges: vec![],
2225            path_to_file_id: HashMap::new(),
2226            linearized_base_contracts: HashMap::new(),
2227            contract_kinds: HashMap::new(),
2228            top_level_importables_by_name: HashMap::new(),
2229            top_level_importables_by_file: HashMap::new(),
2230        }
2231    }
2232
2233    #[test]
2234    fn top_level_importables_include_only_direct_declared_symbols() {
2235        let sources = json!({
2236            "/tmp/A.sol": {
2237                "id": 0,
2238                "ast": {
2239                    "id": 1,
2240                    "nodeType": "SourceUnit",
2241                    "src": "0:100:0",
2242                    "nodes": [
2243                        { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2244                        { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2245                            { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2246                        ] },
2247                        { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2248                        { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2249                        { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2250                        { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2251                        { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2252                        { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2253                    ]
2254                }
2255            }
2256        });
2257
2258        let cache = build_completion_cache(&sources, None);
2259        let map = &cache.top_level_importables_by_name;
2260        let by_file = &cache.top_level_importables_by_file;
2261
2262        assert!(map.contains_key("C"));
2263        assert!(map.contains_key("S"));
2264        assert!(map.contains_key("E"));
2265        assert!(map.contains_key("Wad"));
2266        assert!(map.contains_key("freeFn"));
2267        assert!(map.contains_key("TOP_CONST"));
2268
2269        assert!(!map.contains_key("Alias"));
2270        assert!(!map.contains_key("inside"));
2271        assert!(!map.contains_key("TOP_VAR"));
2272
2273        let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2274        let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2275        assert!(file_names.contains(&"C"));
2276        assert!(file_names.contains(&"TOP_CONST"));
2277        assert!(!file_names.contains(&"Alias"));
2278    }
2279
2280    #[test]
2281    fn top_level_importables_keep_multiple_declarations_for_same_name() {
2282        let sources = json!({
2283            "/tmp/A.sol": {
2284                "id": 0,
2285                "ast": {
2286                    "id": 1,
2287                    "nodeType": "SourceUnit",
2288                    "src": "0:100:0",
2289                    "nodes": [
2290                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2291                    ]
2292                }
2293            },
2294            "/tmp/B.sol": {
2295                "id": 1,
2296                "ast": {
2297                    "id": 2,
2298                    "nodeType": "SourceUnit",
2299                    "src": "0:100:1",
2300                    "nodes": [
2301                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2302                    ]
2303                }
2304            }
2305        });
2306
2307        let cache = build_completion_cache(&sources, None);
2308        let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2309        assert_eq!(entries.len(), 2);
2310    }
2311
2312    #[test]
2313    fn extract_top_level_importables_for_file_finds_expected_symbols() {
2314        let ast = json!({
2315            "id": 1,
2316            "nodeType": "SourceUnit",
2317            "src": "0:100:0",
2318            "nodes": [
2319                { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2320                { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2321                { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2322            ]
2323        });
2324
2325        let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2326        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2327        assert!(names.contains(&"f"));
2328        assert!(names.contains(&"K"));
2329        assert!(!names.contains(&"V"));
2330    }
2331
2332    #[test]
2333    fn top_level_importables_can_be_replaced_per_file() {
2334        let sources = json!({
2335            "/tmp/A.sol": {
2336                "id": 0,
2337                "ast": {
2338                    "id": 1,
2339                    "nodeType": "SourceUnit",
2340                    "src": "0:100:0",
2341                    "nodes": [
2342                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2343                    ]
2344                }
2345            },
2346            "/tmp/B.sol": {
2347                "id": 1,
2348                "ast": {
2349                    "id": 2,
2350                    "nodeType": "SourceUnit",
2351                    "src": "0:100:1",
2352                    "nodes": [
2353                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2354                    ]
2355                }
2356            }
2357        });
2358
2359        let mut cache = build_completion_cache(&sources, None);
2360        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2361
2362        cache.replace_top_level_importables_for_path(
2363            "/tmp/A.sol".to_string(),
2364            vec![TopLevelImportable {
2365                name: "newA".to_string(),
2366                declaring_path: "/tmp/A.sol".to_string(),
2367                node_type: "FunctionDefinition".to_string(),
2368                kind: CompletionItemKind::FUNCTION,
2369            }],
2370        );
2371        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2372        assert!(cache.top_level_importables_by_name.contains_key("newA"));
2373
2374        cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2375        assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2376    }
2377
2378    #[test]
2379    fn append_auto_import_candidates_last_sets_tail_sort_text() {
2380        let base = vec![CompletionItem {
2381            label: "localVar".to_string(),
2382            ..Default::default()
2383        }];
2384        let auto = vec![CompletionItem {
2385            label: "ImportMe".to_string(),
2386            ..Default::default()
2387        }];
2388
2389        let out = append_auto_import_candidates_last(base, auto);
2390        assert_eq!(out.len(), 2);
2391        assert_eq!(out[1].label, "ImportMe");
2392        assert!(
2393            out[1]
2394                .sort_text
2395                .as_deref()
2396                .is_some_and(|s| s.starts_with("zz_autoimport_"))
2397        );
2398    }
2399
2400    #[test]
2401    fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2402        let base = vec![CompletionItem {
2403            label: "B".to_string(),
2404            ..Default::default()
2405        }];
2406        let auto = vec![
2407            CompletionItem {
2408                label: "B".to_string(),
2409                detail: Some("ContractDefinition (./B.sol)".to_string()),
2410                ..Default::default()
2411            },
2412            CompletionItem {
2413                label: "B".to_string(),
2414                detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2415                ..Default::default()
2416            },
2417        ];
2418
2419        let out = append_auto_import_candidates_last(base, auto);
2420        assert_eq!(out.len(), 3);
2421    }
2422
2423    #[test]
2424    fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2425        let base = vec![CompletionItem {
2426            label: "B".to_string(),
2427            ..Default::default()
2428        }];
2429        let auto = vec![CompletionItem {
2430            label: "B".to_string(),
2431            additional_text_edits: Some(vec![TextEdit {
2432                range: Range {
2433                    start: Position {
2434                        line: 0,
2435                        character: 0,
2436                    },
2437                    end: Position {
2438                        line: 0,
2439                        character: 0,
2440                    },
2441                },
2442                new_text: "import {B} from \"./B.sol\";\n".to_string(),
2443            }]),
2444            ..Default::default()
2445        }];
2446        let out = append_auto_import_candidates_last(base, auto);
2447        assert!(
2448            out[0].additional_text_edits.is_some(),
2449            "base item should inherit unique import edit"
2450        );
2451    }
2452
2453    #[test]
2454    fn top_level_importable_candidates_include_import_edit() {
2455        let mut cache = empty_cache();
2456        cache.top_level_importables_by_name.insert(
2457            "B".to_string(),
2458            vec![TopLevelImportable {
2459                name: "B".to_string(),
2460                declaring_path: "/tmp/example/B.sol".to_string(),
2461                node_type: "ContractDefinition".to_string(),
2462                kind: CompletionItemKind::CLASS,
2463            }],
2464        );
2465
2466        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2467        let items = super::top_level_importable_completion_candidates(
2468            &cache,
2469            Some("/tmp/example/A.sol"),
2470            source,
2471        );
2472        assert_eq!(items.len(), 1);
2473        let edit_text = items[0]
2474            .additional_text_edits
2475            .as_ref()
2476            .and_then(|edits| edits.first())
2477            .map(|e| e.new_text.clone())
2478            .unwrap_or_default();
2479        assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2480    }
2481
2482    #[test]
2483    fn handle_completion_general_path_keeps_base_items() {
2484        let mut cache = empty_cache();
2485        cache.general_completions = vec![CompletionItem {
2486            label: "A".to_string(),
2487            ..Default::default()
2488        }];
2489
2490        let resp = super::handle_completion(
2491            Some(&cache),
2492            "contract X {}",
2493            Position {
2494                line: 0,
2495                character: 0,
2496            },
2497            None,
2498            None,
2499        );
2500        match resp {
2501            Some(CompletionResponse::List(list)) => {
2502                assert_eq!(list.items.len(), 1);
2503                assert_eq!(list.items[0].label, "A");
2504            }
2505            _ => panic!("expected completion list"),
2506        }
2507    }
2508
2509    // --- import path completion tests ---
2510
2511    #[test]
2512    fn make_import_item_has_filter_text_and_text_edit() {
2513        let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
2514        assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
2515        assert!(item.text_edit.is_some(), "text_edit should be set");
2516        assert!(
2517            item.insert_text.is_none(),
2518            "insert_text should be absent when text_edit is set"
2519        );
2520    }
2521
2522    #[test]
2523    fn all_sol_import_paths_no_panic_missing_dir() {
2524        let current = std::path::Path::new("/nonexistent/src/Foo.sol");
2525        let root = std::path::Path::new("/nonexistent");
2526        let items = super::all_sol_import_paths(current, root, &[], None);
2527        assert!(items.is_empty());
2528    }
2529
2530    #[test]
2531    fn all_sol_import_paths_relative_labels() {
2532        // Use real paths from the repo fixture
2533        let root = std::path::Path::new("example");
2534        let current = root.join("A.sol");
2535        let items = super::all_sol_import_paths(&current, root, &[], None);
2536        // All labels should start with ./ or ../
2537        for item in &items {
2538            assert!(
2539                item.label.starts_with("./") || item.label.starts_with("../"),
2540                "label should be relative: {}",
2541                item.label
2542            );
2543            assert!(
2544                item.label.ends_with(".sol"),
2545                "label should end with .sol: {}",
2546                item.label
2547            );
2548        }
2549        assert!(!items.is_empty(), "should find at least one .sol file");
2550    }
2551}