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