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, CompletionTextEdit,
6    Position, Range, 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
1725fn completion_item(
1726    label: impl Into<String>,
1727    kind: CompletionItemKind,
1728    detail: impl Into<String>,
1729) -> CompletionItem {
1730    CompletionItem {
1731        label: label.into(),
1732        kind: Some(kind),
1733        detail: Some(detail.into()),
1734        ..Default::default()
1735    }
1736}
1737
1738fn line_replacement_item(
1739    label: impl Into<String>,
1740    detail: impl Into<String>,
1741    new_text: impl Into<String>,
1742    position: Position,
1743) -> CompletionItem {
1744    let label = label.into();
1745    let new_text = new_text.into();
1746    CompletionItem {
1747        filter_text: Some(format!("spdx {label} {new_text}")),
1748        label,
1749        kind: Some(CompletionItemKind::CONSTANT),
1750        detail: Some(detail.into()),
1751        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1752            range: Range {
1753                start: Position {
1754                    line: position.line,
1755                    character: 0,
1756                },
1757                end: position,
1758            },
1759            new_text,
1760        })),
1761        ..Default::default()
1762    }
1763}
1764
1765fn prefix_at_byte(line: &str, col_byte: u32) -> &str {
1766    let mut end = (col_byte as usize).min(line.len());
1767    while !line.is_char_boundary(end) {
1768        end -= 1;
1769    }
1770    &line[..end]
1771}
1772
1773fn source_unit_directive_completions(
1774    line: &str,
1775    line_prefix: &str,
1776    position: Position,
1777) -> Option<Vec<CompletionItem>> {
1778    let trimmed = line_prefix.trim_start();
1779
1780    if let Some(after_slashes) = trimmed.strip_prefix("//") {
1781        let body = after_slashes.trim_start();
1782        let upper = body.to_ascii_uppercase();
1783
1784        if upper.starts_with("SPDX-LICENSE-IDENTIFIER:") {
1785            return Some(spdx_license_identifier_completions());
1786        }
1787
1788        if is_spdx_directive_prefix(&upper) {
1789            return Some(spdx_directive_completions(position));
1790        }
1791    }
1792
1793    if let Some(after_solidity) = strip_directive_value(trimmed, "pragma solidity") {
1794        if after_solidity.contains(';') {
1795            return Some(vec![]);
1796        }
1797
1798        return Some(solidity_version_completions(
1799            position,
1800            directive_value_range(line, "pragma solidity"),
1801        ));
1802    }
1803
1804    if let Some(after_abicoder) = strip_directive_value(trimmed, "pragma abicoder") {
1805        if after_abicoder.contains(';') {
1806            return Some(vec![]);
1807        }
1808
1809        return Some(vec![
1810            completion_item("v2", CompletionItemKind::CONSTANT, "ABI coder v2"),
1811            completion_item("v1", CompletionItemKind::CONSTANT, "ABI coder v1"),
1812        ]);
1813    }
1814
1815    if let Some(after_pragma) = trimmed.strip_prefix("pragma")
1816        && !after_pragma.is_empty()
1817        && after_pragma.trim().is_empty()
1818    {
1819        return Some(vec![
1820            completion_item(
1821                "solidity",
1822                CompletionItemKind::KEYWORD,
1823                "Compiler version pragma",
1824            ),
1825            completion_item("abicoder", CompletionItemKind::KEYWORD, "ABI coder pragma"),
1826        ]);
1827    }
1828
1829    None
1830}
1831
1832fn strip_directive_value<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
1833    let rest = line.strip_prefix(directive)?;
1834    if rest.is_empty() || rest.starts_with(char::is_whitespace) {
1835        Some(rest)
1836    } else {
1837        None
1838    }
1839}
1840
1841fn is_spdx_directive_prefix(upper_comment_body: &str) -> bool {
1842    let body = upper_comment_body.trim_start();
1843    !body.is_empty() && ("SPDX".starts_with(body) || body.starts_with("SPDX"))
1844}
1845
1846fn spdx_directive_completions(position: Position) -> Vec<CompletionItem> {
1847    vec![
1848        line_replacement_item(
1849            "// SPDX-License-Identifier: MIT",
1850            "SPDX license identifier",
1851            "// SPDX-License-Identifier: MIT",
1852            position,
1853        ),
1854        line_replacement_item(
1855            "// SPDX-License-Identifier: UNLICENSED",
1856            "SPDX license identifier",
1857            "// SPDX-License-Identifier: UNLICENSED",
1858            position,
1859        ),
1860        line_replacement_item(
1861            "// SPDX-License-Identifier: Apache-2.0",
1862            "SPDX license identifier",
1863            "// SPDX-License-Identifier: Apache-2.0",
1864            position,
1865        ),
1866    ]
1867}
1868
1869fn spdx_license_identifier_completions() -> Vec<CompletionItem> {
1870    [
1871        "MIT",
1872        "UNLICENSED",
1873        "Apache-2.0",
1874        "GPL-3.0-only",
1875        "GPL-3.0-or-later",
1876        "BSD-3-Clause",
1877    ]
1878    .into_iter()
1879    .map(|license| completion_item(license, CompletionItemKind::CONSTANT, "SPDX license"))
1880    .collect()
1881}
1882
1883fn directive_value_range(line: &str, directive: &str) -> Range {
1884    let mut byte = line
1885        .find(directive)
1886        .map(|idx| idx + directive.len())
1887        .unwrap_or(line.len());
1888
1889    while byte < line.len() {
1890        let ch = line[byte..]
1891            .chars()
1892            .next()
1893            .expect("byte is within string bounds");
1894        if !ch.is_whitespace() {
1895            break;
1896        }
1897        byte += ch.len_utf8();
1898    }
1899
1900    let end_byte = line[byte..]
1901        .find(';')
1902        .map(|idx| byte + idx)
1903        .unwrap_or(line.len());
1904
1905    Range {
1906        start: Position {
1907            line: 0,
1908            character: line[..byte].encode_utf16().count() as u32,
1909        },
1910        end: Position {
1911            line: 0,
1912            character: line[..end_byte].encode_utf16().count() as u32,
1913        },
1914    }
1915}
1916
1917fn version_completion_item(
1918    label: impl Into<String>,
1919    detail: impl Into<String>,
1920    position: Position,
1921    range: Range,
1922) -> CompletionItem {
1923    let label = label.into();
1924    CompletionItem {
1925        label: label.clone(),
1926        kind: Some(CompletionItemKind::CONSTANT),
1927        detail: Some(detail.into()),
1928        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1929            range: Range {
1930                start: Position {
1931                    line: position.line,
1932                    character: range.start.character,
1933                },
1934                end: Position {
1935                    line: position.line,
1936                    character: range.end.character,
1937                },
1938            },
1939            new_text: label,
1940        })),
1941        ..Default::default()
1942    }
1943}
1944
1945fn solidity_version_completions(position: Position, range: Range) -> Vec<CompletionItem> {
1946    [
1947        ("^0.8.35", "Latest Solidity 0.8.x compatible range"),
1948        ("0.8.35", "Latest Solidity exact version"),
1949        ("^0.8.0", "Solidity 0.8.x compatible range"),
1950        (">=0.8.0 <0.9.0", "Solidity 0.8.x range"),
1951    ]
1952    .into_iter()
1953    .map(|(version, detail)| version_completion_item(version, detail, position, range))
1954    .collect()
1955}
1956
1957/// Append auto-import candidates at the tail of completion results.
1958///
1959/// This enforces lower priority ordering by:
1960/// 1) appending after base completions
1961/// 2) assigning high `sortText` values (`zz_autoimport_*`) when missing
1962pub fn append_auto_import_candidates_last(
1963    mut base: Vec<CompletionItem>,
1964    mut auto_import_candidates: Vec<CompletionItem>,
1965) -> Vec<CompletionItem> {
1966    let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1967    for item in &auto_import_candidates {
1968        let entry = unique_label_edits
1969            .entry(item.label.clone())
1970            .or_insert_with(|| item.additional_text_edits.clone());
1971        if *entry != item.additional_text_edits {
1972            *entry = None;
1973        }
1974    }
1975
1976    // If a label maps to exactly one import edit, attach it to the corresponding
1977    // base completion item too. This ensures accepting the normal item can still
1978    // apply import edits in clients that de-prioritize or collapse duplicate labels.
1979    for item in &mut base {
1980        if item.additional_text_edits.is_none()
1981            && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1982        {
1983            item.additional_text_edits = Some(edits.clone());
1984        }
1985    }
1986
1987    for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1988        if item.sort_text.is_none() {
1989            item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1990        }
1991    }
1992
1993    base.extend(auto_import_candidates);
1994    base
1995}
1996
1997/// Convert cached top-level importable symbols into completion items.
1998///
1999/// These are intended as low-priority tail candidates appended after normal
2000/// per-file completions.
2001pub fn top_level_importable_completion_candidates(
2002    cache: &CompletionCache,
2003    current_file_path: Option<&str>,
2004    source_text: &str,
2005) -> Vec<CompletionItem> {
2006    let mut out = Vec::new();
2007    for symbols in cache.top_level_importables_by_name.values() {
2008        for symbol in symbols {
2009            if let Some(cur) = current_file_path
2010                && cur == symbol.declaring_path
2011            {
2012                continue;
2013            }
2014
2015            let import_path = match current_file_path.and_then(|cur| {
2016                to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
2017            }) {
2018                Some(p) => p,
2019                None => continue,
2020            };
2021
2022            if import_statement_already_present(source_text, &symbol.name, &import_path) {
2023                continue;
2024            }
2025
2026            let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
2027            out.push(CompletionItem {
2028                label: symbol.name.clone(),
2029                kind: Some(symbol.kind),
2030                detail: Some(format!("{} ({import_path})", symbol.node_type)),
2031                additional_text_edits: import_edit.map(|e| vec![e]),
2032                ..Default::default()
2033            });
2034        }
2035    }
2036    out
2037}
2038
2039fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
2040    let from_dir = current_file.parent()?;
2041    let rel = pathdiff::diff_paths(target_file, from_dir)?;
2042    let mut s = rel.to_string_lossy().replace('\\', "/");
2043    if !s.starts_with("./") && !s.starts_with("../") {
2044        s = format!("./{s}");
2045    }
2046    Some(s)
2047}
2048
2049fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
2050    let named = format!("import {{{symbol}}} from \"{import_path}\";");
2051    let full = format!("import \"{import_path}\";");
2052    source_text.contains(&named) || source_text.contains(&full)
2053}
2054
2055fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
2056    let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
2057    let lines: Vec<&str> = source_text.lines().collect();
2058
2059    let last_import_line = lines
2060        .iter()
2061        .enumerate()
2062        .filter(|(_, line)| line.trim_start().starts_with("import "))
2063        .map(|(idx, _)| idx)
2064        .next_back();
2065
2066    let insert_line = if let Some(idx) = last_import_line {
2067        idx + 1
2068    } else if let Some(idx) = lines
2069        .iter()
2070        .enumerate()
2071        .filter(|(_, line)| line.trim_start().starts_with("pragma "))
2072        .map(|(idx, _)| idx)
2073        .next_back()
2074    {
2075        idx + 1
2076    } else {
2077        0
2078    };
2079
2080    Some(TextEdit {
2081        range: Range {
2082            start: Position {
2083                line: insert_line as u32,
2084                character: 0,
2085            },
2086            end: Position {
2087                line: insert_line as u32,
2088                character: 0,
2089            },
2090        },
2091        new_text: import_stmt,
2092    })
2093}
2094
2095/// Handle a completion request with optional tail candidates.
2096///
2097/// Tail candidates are only appended for non-dot completions and are always
2098/// ordered last via `append_auto_import_candidates_last`.
2099pub fn handle_completion_with_tail_candidates(
2100    cache: Option<&CompletionCache>,
2101    source_text: &str,
2102    position: Position,
2103    trigger_char: Option<&str>,
2104    file_id: Option<FileId>,
2105    tail_candidates: Vec<CompletionItem>,
2106) -> Option<CompletionResponse> {
2107    let lines: Vec<&str> = source_text.lines().collect();
2108    let line = lines.get(position.line as usize)?;
2109
2110    // Convert encoding-aware column to a byte offset within this line.
2111    let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
2112    let line_start_byte: usize = source_text[..abs_byte]
2113        .rfind('\n')
2114        .map(|i| i + 1)
2115        .unwrap_or(0);
2116    let col_byte = (abs_byte - line_start_byte) as u32;
2117
2118    if let Some(items) =
2119        source_unit_directive_completions(line, prefix_at_byte(line, col_byte), position)
2120    {
2121        return Some(CompletionResponse::List(CompletionList {
2122            is_incomplete: false,
2123            items,
2124        }));
2125    }
2126
2127    // Build scope context for scope-aware type resolution
2128    let scope_ctx = file_id.map(|fid| ScopeContext {
2129        byte_pos: abs_byte,
2130        file_id: fid,
2131    });
2132
2133    let items = if trigger_char == Some(".") {
2134        let chain = parse_dot_chain(line, col_byte);
2135        if chain.is_empty() {
2136            return None;
2137        }
2138        match cache {
2139            Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
2140            None => {
2141                // No cache yet — serve magic dot completions (msg., block., etc.)
2142                if chain.len() == 1 {
2143                    let seg = &chain[0];
2144                    if seg.name == "type" && seg.kind == AccessKind::Call {
2145                        // type(X). without cache — classify based on name alone
2146                        type_meta_members(seg.call_args.as_deref(), None)
2147                    } else if seg.kind == AccessKind::Plain {
2148                        magic_members(&seg.name).unwrap_or_default()
2149                    } else {
2150                        vec![]
2151                    }
2152                } else {
2153                    vec![]
2154                }
2155            }
2156        }
2157    } else {
2158        match cache {
2159            Some(c) => {
2160                append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
2161            }
2162            None => get_static_completions(),
2163        }
2164    };
2165
2166    Some(CompletionResponse::List(CompletionList {
2167        is_incomplete: cache.is_none(),
2168        items,
2169    }))
2170}
2171
2172/// Handle a completion request.
2173///
2174/// When `cache` is `Some`, full AST-aware completions are returned.
2175/// When `cache` is `None`, only static completions (keywords, globals, units)
2176/// and magic dot completions (msg., block., tx., abi., type().) are returned
2177/// immediately — no blocking.
2178///
2179/// `file_id` is the AST source file id, needed for scope-aware resolution.
2180/// When `None`, scope resolution is skipped and flat lookup is used.
2181pub fn handle_completion(
2182    cache: Option<&CompletionCache>,
2183    source_text: &str,
2184    position: Position,
2185    trigger_char: Option<&str>,
2186    file_id: Option<FileId>,
2187) -> Option<CompletionResponse> {
2188    handle_completion_with_tail_candidates(
2189        cache,
2190        source_text,
2191        position,
2192        trigger_char,
2193        file_id,
2194        vec![],
2195    )
2196}
2197
2198const SOLIDITY_KEYWORDS: &[&str] = &[
2199    "abstract",
2200    "address",
2201    "assembly",
2202    "bool",
2203    "break",
2204    "bytes",
2205    "bytes1",
2206    "bytes4",
2207    "bytes32",
2208    "calldata",
2209    "constant",
2210    "constructor",
2211    "continue",
2212    "contract",
2213    "delete",
2214    "do",
2215    "else",
2216    "emit",
2217    "enum",
2218    "error",
2219    "event",
2220    "external",
2221    "fallback",
2222    "false",
2223    "for",
2224    "function",
2225    "if",
2226    "immutable",
2227    "import",
2228    "indexed",
2229    "int8",
2230    "int24",
2231    "int128",
2232    "int256",
2233    "interface",
2234    "internal",
2235    "library",
2236    "mapping",
2237    "memory",
2238    "modifier",
2239    "new",
2240    "override",
2241    "payable",
2242    "pragma",
2243    "private",
2244    "public",
2245    "pure",
2246    "receive",
2247    "return",
2248    "returns",
2249    "revert",
2250    "storage",
2251    "string",
2252    "struct",
2253    "true",
2254    "type",
2255    "uint8",
2256    "uint24",
2257    "uint128",
2258    "uint160",
2259    "uint256",
2260    "unchecked",
2261    "using",
2262    "view",
2263    "virtual",
2264    "while",
2265];
2266
2267/// Ether denomination units — suffixes for literal numbers.
2268const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2269
2270/// Time units — suffixes for literal numbers.
2271const TIME_UNITS: &[(&str, &str)] = &[
2272    ("seconds", "1"),
2273    ("minutes", "60 seconds"),
2274    ("hours", "3600 seconds"),
2275    ("days", "86400 seconds"),
2276    ("weeks", "604800 seconds"),
2277];
2278
2279const MAGIC_GLOBALS: &[(&str, &str)] = &[
2280    ("msg", "msg"),
2281    ("block", "block"),
2282    ("tx", "tx"),
2283    ("abi", "abi"),
2284    ("this", "address"),
2285    ("super", "contract"),
2286    ("type", "type information"),
2287];
2288
2289const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2290    // Mathematical and Cryptographic Functions
2291    ("addmod(uint256, uint256, uint256)", "uint256"),
2292    ("mulmod(uint256, uint256, uint256)", "uint256"),
2293    ("keccak256(bytes memory)", "bytes32"),
2294    ("sha256(bytes memory)", "bytes32"),
2295    ("ripemd160(bytes memory)", "bytes20"),
2296    (
2297        "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2298        "address",
2299    ),
2300    // Block and Transaction Properties (functions)
2301    ("blockhash(uint256 blockNumber)", "bytes32"),
2302    ("blobhash(uint256 index)", "bytes32"),
2303    ("gasleft()", "uint256"),
2304    // Error Handling
2305    ("assert(bool condition)", ""),
2306    ("require(bool condition)", ""),
2307    ("require(bool condition, string memory message)", ""),
2308    ("revert()", ""),
2309    ("revert(string memory reason)", ""),
2310    // Contract-related
2311    ("selfdestruct(address payable recipient)", ""),
2312];
2313
2314// ---------------------------------------------------------------------------
2315// Import path completions
2316// ---------------------------------------------------------------------------
2317
2318/// Walk `project_root` recursively and return every `.sol` file as:
2319///   1. A relative path from the current file's directory (e.g. `./libraries/Pool.sol`)
2320///   2. A remapped path for each matching remapping (e.g. `forge-std/Test.sol`)
2321///
2322/// Skips `out/`, `cache/`, `node_modules/`, `.git/`.
2323///
2324/// `typed_range` is `Some((line, start_col, end_col))` where `start_col` is the
2325/// character column right after the opening quote and `end_col` is the cursor
2326/// column. When provided, each item gets a `text_edit` that replaces the
2327/// already-typed text so the insert doesn't double-up, plus `filter_text = label`
2328/// so clients filter on the full path rather than the word at cursor.
2329pub fn all_sol_import_paths(
2330    current_file: &Path,
2331    project_root: &Path,
2332    remappings: &[String],
2333    typed_range: Option<(u32, u32, u32)>,
2334) -> Vec<CompletionItem> {
2335    let current_dir = match current_file.parent() {
2336        Some(d) => d,
2337        None => return vec![],
2338    };
2339
2340    // Pre-parse remappings into (prefix, abs_target_path) pairs.
2341    // e.g. "forge-std/=lib/forge-std/src/" → ("forge-std/", "/project/lib/forge-std/src/")
2342    // Canonicalize the target so strip_prefix works even when the path contains
2343    // `..` segments (e.g. lib/forge-std/../src/ would otherwise produce bad labels).
2344    let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2345        .iter()
2346        .filter_map(|r| {
2347            let mut it = r.splitn(2, '=');
2348            let prefix = it.next()?.to_string();
2349            let target = it.next()?;
2350            if prefix.is_empty() || target.is_empty() {
2351                return None;
2352            }
2353            let raw = project_root.join(target);
2354            let canonical = raw.canonicalize().unwrap_or(raw);
2355            Some((prefix, canonical))
2356        })
2357        .collect();
2358
2359    let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2360    let mut items = Vec::new();
2361
2362    collect_sol_files(
2363        project_root,
2364        current_dir,
2365        &parsed_remappings,
2366        skip_dirs,
2367        typed_range,
2368        &mut items,
2369    );
2370    items.sort_by(|a, b| a.label.cmp(&b.label));
2371    items
2372}
2373
2374fn collect_sol_files(
2375    dir: &Path,
2376    current_dir: &Path,
2377    remappings: &[(String, std::path::PathBuf)],
2378    skip_dirs: &[&str],
2379    typed_range: Option<(u32, u32, u32)>,
2380    out: &mut Vec<CompletionItem>,
2381) {
2382    let entries = match std::fs::read_dir(dir) {
2383        Ok(e) => e,
2384        Err(_) => return,
2385    };
2386    for entry in entries.flatten() {
2387        let path = entry.path();
2388        let name = entry.file_name();
2389        let name_str = name.to_string_lossy();
2390
2391        if name_str.starts_with('.') {
2392            continue;
2393        }
2394
2395        if path.is_dir() {
2396            if skip_dirs.contains(&name_str.as_ref()) {
2397                continue;
2398            }
2399            collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2400            continue;
2401        }
2402
2403        if !path.is_file() || !name_str.ends_with(".sol") {
2404            continue;
2405        }
2406
2407        // 1. Relative path (always emitted)
2408        if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2409            let s = rel.to_string_lossy().to_string();
2410            let label = if s.starts_with("../") || s.starts_with("./") {
2411                s
2412            } else {
2413                format!("./{s}")
2414            };
2415            out.push(make_import_item(label, typed_range));
2416        }
2417
2418        // 2. Remapped paths — for every remapping whose target is a prefix of
2419        //    this file's absolute path, emit the remapped form.
2420        //    e.g. file=/project/lib/forge-std/src/Test.sol
2421        //         remap forge-std/=/project/lib/forge-std/src/
2422        //         → label = "forge-std/Test.sol"
2423        for (prefix, target_abs) in remappings {
2424            if let Ok(suffix) = path.strip_prefix(target_abs) {
2425                let suffix_str = suffix
2426                    .to_string_lossy()
2427                    .trim_start_matches(['/', '\\'])
2428                    .to_string();
2429                let label = format!("{}{}", prefix, suffix_str);
2430                out.push(make_import_item(label, typed_range));
2431            }
2432        }
2433    }
2434}
2435
2436fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2437    // filter_text = label so that clients whose word-at-cursor is shorter than
2438    // the full path (e.g. just "Pool" when the label is "./src/Pool.sol") still
2439    // match correctly via substring/fuzzy matching against the full path.
2440    //
2441    // text_edit replaces the already-typed text inside the quotes so the insert
2442    // doesn't append after what was already typed.
2443    let text_edit = typed_range.map(|(line, start_col, end_col)| {
2444        tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2445            range: Range {
2446                start: Position {
2447                    line,
2448                    character: start_col,
2449                },
2450                end: Position {
2451                    line,
2452                    character: end_col,
2453                },
2454            },
2455            new_text: label.clone(),
2456        })
2457    });
2458    CompletionItem {
2459        label: label.clone(),
2460        filter_text: Some(label.clone()),
2461        text_edit,
2462        kind: Some(CompletionItemKind::FILE),
2463        ..Default::default()
2464    }
2465}
2466
2467#[cfg(test)]
2468mod tests {
2469    use super::{
2470        CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2471        build_completion_cache, extract_top_level_importables_for_file,
2472    };
2473    use crate::types::SymbolName;
2474    use serde_json::json;
2475    use std::collections::HashMap;
2476    use tower_lsp::lsp_types::CompletionItemKind;
2477    use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2478
2479    fn empty_cache() -> CompletionCache {
2480        CompletionCache {
2481            names: vec![],
2482            name_to_type: HashMap::new(),
2483            node_members: HashMap::new(),
2484            type_to_node: HashMap::new(),
2485            name_to_node_id: HashMap::new(),
2486            method_identifiers: HashMap::new(),
2487            function_return_types: HashMap::new(),
2488            using_for: HashMap::new(),
2489            using_for_wildcard: vec![],
2490            general_completions: vec![],
2491            scope_declarations: HashMap::new(),
2492            scope_parent: HashMap::new(),
2493            scope_ranges: vec![],
2494            path_to_file_id: HashMap::new(),
2495            linearized_base_contracts: HashMap::new(),
2496            contract_kinds: HashMap::new(),
2497            top_level_importables_by_name: HashMap::new(),
2498            top_level_importables_by_file: HashMap::new(),
2499        }
2500    }
2501
2502    #[test]
2503    fn top_level_importables_include_only_direct_declared_symbols() {
2504        let sources = json!({
2505            "/tmp/A.sol": {
2506                "id": 0,
2507                "ast": {
2508                    "id": 1,
2509                    "nodeType": "SourceUnit",
2510                    "src": "0:100:0",
2511                    "nodes": [
2512                        { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2513                        { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2514                            { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2515                        ] },
2516                        { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2517                        { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2518                        { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2519                        { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2520                        { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2521                        { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2522                    ]
2523                }
2524            }
2525        });
2526
2527        let cache = build_completion_cache(&sources, None, None);
2528        let map = &cache.top_level_importables_by_name;
2529        let by_file = &cache.top_level_importables_by_file;
2530
2531        assert!(map.contains_key("C"));
2532        assert!(map.contains_key("S"));
2533        assert!(map.contains_key("E"));
2534        assert!(map.contains_key("Wad"));
2535        assert!(map.contains_key("freeFn"));
2536        assert!(map.contains_key("TOP_CONST"));
2537
2538        assert!(!map.contains_key("Alias"));
2539        assert!(!map.contains_key("inside"));
2540        assert!(!map.contains_key("TOP_VAR"));
2541
2542        let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2543        let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2544        assert!(file_names.contains(&"C"));
2545        assert!(file_names.contains(&"TOP_CONST"));
2546        assert!(!file_names.contains(&"Alias"));
2547    }
2548
2549    #[test]
2550    fn top_level_importables_keep_multiple_declarations_for_same_name() {
2551        let sources = json!({
2552            "/tmp/A.sol": {
2553                "id": 0,
2554                "ast": {
2555                    "id": 1,
2556                    "nodeType": "SourceUnit",
2557                    "src": "0:100:0",
2558                    "nodes": [
2559                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2560                    ]
2561                }
2562            },
2563            "/tmp/B.sol": {
2564                "id": 1,
2565                "ast": {
2566                    "id": 2,
2567                    "nodeType": "SourceUnit",
2568                    "src": "0:100:1",
2569                    "nodes": [
2570                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2571                    ]
2572                }
2573            }
2574        });
2575
2576        let cache = build_completion_cache(&sources, None, None);
2577        let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2578        assert_eq!(entries.len(), 2);
2579    }
2580
2581    #[test]
2582    fn extract_top_level_importables_for_file_finds_expected_symbols() {
2583        let ast = json!({
2584            "id": 1,
2585            "nodeType": "SourceUnit",
2586            "src": "0:100:0",
2587            "nodes": [
2588                { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2589                { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2590                { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2591            ]
2592        });
2593
2594        let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2595        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2596        assert!(names.contains(&"f"));
2597        assert!(names.contains(&"K"));
2598        assert!(!names.contains(&"V"));
2599    }
2600
2601    #[test]
2602    fn top_level_importables_can_be_replaced_per_file() {
2603        let sources = json!({
2604            "/tmp/A.sol": {
2605                "id": 0,
2606                "ast": {
2607                    "id": 1,
2608                    "nodeType": "SourceUnit",
2609                    "src": "0:100:0",
2610                    "nodes": [
2611                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2612                    ]
2613                }
2614            },
2615            "/tmp/B.sol": {
2616                "id": 1,
2617                "ast": {
2618                    "id": 2,
2619                    "nodeType": "SourceUnit",
2620                    "src": "0:100:1",
2621                    "nodes": [
2622                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2623                    ]
2624                }
2625            }
2626        });
2627
2628        let mut cache = build_completion_cache(&sources, None, None);
2629        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2630
2631        cache.replace_top_level_importables_for_path(
2632            "/tmp/A.sol".to_string(),
2633            vec![TopLevelImportable {
2634                name: "newA".to_string(),
2635                declaring_path: "/tmp/A.sol".to_string(),
2636                node_type: "FunctionDefinition".to_string(),
2637                kind: CompletionItemKind::FUNCTION,
2638            }],
2639        );
2640        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2641        assert!(cache.top_level_importables_by_name.contains_key("newA"));
2642
2643        cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2644        assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2645    }
2646
2647    #[test]
2648    fn append_auto_import_candidates_last_sets_tail_sort_text() {
2649        let base = vec![CompletionItem {
2650            label: "localVar".to_string(),
2651            ..Default::default()
2652        }];
2653        let auto = vec![CompletionItem {
2654            label: "ImportMe".to_string(),
2655            ..Default::default()
2656        }];
2657
2658        let out = append_auto_import_candidates_last(base, auto);
2659        assert_eq!(out.len(), 2);
2660        assert_eq!(out[1].label, "ImportMe");
2661        assert!(
2662            out[1]
2663                .sort_text
2664                .as_deref()
2665                .is_some_and(|s| s.starts_with("zz_autoimport_"))
2666        );
2667    }
2668
2669    #[test]
2670    fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2671        let base = vec![CompletionItem {
2672            label: "B".to_string(),
2673            ..Default::default()
2674        }];
2675        let auto = vec![
2676            CompletionItem {
2677                label: "B".to_string(),
2678                detail: Some("ContractDefinition (./B.sol)".to_string()),
2679                ..Default::default()
2680            },
2681            CompletionItem {
2682                label: "B".to_string(),
2683                detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2684                ..Default::default()
2685            },
2686        ];
2687
2688        let out = append_auto_import_candidates_last(base, auto);
2689        assert_eq!(out.len(), 3);
2690    }
2691
2692    #[test]
2693    fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2694        let base = vec![CompletionItem {
2695            label: "B".to_string(),
2696            ..Default::default()
2697        }];
2698        let auto = vec![CompletionItem {
2699            label: "B".to_string(),
2700            additional_text_edits: Some(vec![TextEdit {
2701                range: Range {
2702                    start: Position {
2703                        line: 0,
2704                        character: 0,
2705                    },
2706                    end: Position {
2707                        line: 0,
2708                        character: 0,
2709                    },
2710                },
2711                new_text: "import {B} from \"./B.sol\";\n".to_string(),
2712            }]),
2713            ..Default::default()
2714        }];
2715        let out = append_auto_import_candidates_last(base, auto);
2716        assert!(
2717            out[0].additional_text_edits.is_some(),
2718            "base item should inherit unique import edit"
2719        );
2720    }
2721
2722    #[test]
2723    fn top_level_importable_candidates_include_import_edit() {
2724        let mut cache = empty_cache();
2725        cache.top_level_importables_by_name.insert(
2726            SymbolName::new("B"),
2727            vec![TopLevelImportable {
2728                name: "B".to_string(),
2729                declaring_path: "/tmp/example/B.sol".to_string(),
2730                node_type: "ContractDefinition".to_string(),
2731                kind: CompletionItemKind::CLASS,
2732            }],
2733        );
2734
2735        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2736        let items = super::top_level_importable_completion_candidates(
2737            &cache,
2738            Some("/tmp/example/A.sol"),
2739            source,
2740        );
2741        assert_eq!(items.len(), 1);
2742        let edit_text = items[0]
2743            .additional_text_edits
2744            .as_ref()
2745            .and_then(|edits| edits.first())
2746            .map(|e| e.new_text.clone())
2747            .unwrap_or_default();
2748        assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2749    }
2750
2751    #[test]
2752    fn handle_completion_general_path_keeps_base_items() {
2753        let mut cache = empty_cache();
2754        cache.general_completions = vec![CompletionItem {
2755            label: "A".to_string(),
2756            ..Default::default()
2757        }];
2758
2759        let resp = super::handle_completion(
2760            Some(&cache),
2761            "contract X {}",
2762            Position {
2763                line: 0,
2764                character: 0,
2765            },
2766            None,
2767            None,
2768        );
2769        match resp {
2770            Some(CompletionResponse::List(list)) => {
2771                assert_eq!(list.items.len(), 1);
2772                assert_eq!(list.items[0].label, "A");
2773            }
2774            _ => panic!("expected completion list"),
2775        }
2776    }
2777
2778    // --- import path completion tests ---
2779
2780    #[test]
2781    fn make_import_item_has_filter_text_and_text_edit() {
2782        let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
2783        assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
2784        assert!(item.text_edit.is_some(), "text_edit should be set");
2785        assert!(
2786            item.insert_text.is_none(),
2787            "insert_text should be absent when text_edit is set"
2788        );
2789    }
2790
2791    #[test]
2792    fn all_sol_import_paths_no_panic_missing_dir() {
2793        let current = std::path::Path::new("/nonexistent/src/Foo.sol");
2794        let root = std::path::Path::new("/nonexistent");
2795        let items = super::all_sol_import_paths(current, root, &[], None);
2796        assert!(items.is_empty());
2797    }
2798
2799    #[test]
2800    fn all_sol_import_paths_relative_labels() {
2801        // Use real paths from the repo fixture
2802        let root = std::path::Path::new("example");
2803        let current = root.join("A.sol");
2804        let items = super::all_sol_import_paths(&current, root, &[], None);
2805        // All labels should start with ./ or ../
2806        for item in &items {
2807            assert!(
2808                item.label.starts_with("./") || item.label.starts_with("../"),
2809                "label should be relative: {}",
2810                item.label
2811            );
2812            assert!(
2813                item.label.ends_with(".sol"),
2814                "label should end with .sol: {}",
2815                item.label
2816            );
2817        }
2818        assert!(!items.is_empty(), "should find at least one .sol file");
2819    }
2820}