Skip to main content

solidity_language_server/
completion.rs

1use serde_json::Value;
2use std::collections::{HashMap, HashSet};
3use std::ops::ControlFlow;
4use std::path::Path;
5use tower_lsp::lsp_types::{
6    CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, CompletionTextEdit,
7    Position, Range, TextEdit,
8};
9
10use crate::goto::CHILD_KEYS;
11use crate::hover::build_function_signature;
12use crate::types::{FileId, NodeId, RelPath, SourceLoc, SymbolName, TypeIdentifier};
13use crate::utils::push_if_node_or_array;
14
15/// A directly-declared top-level symbol that can be imported.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TopLevelImportable {
18    /// Symbol name.
19    pub name: String,
20    /// Absolute source path where the symbol is declared.
21    pub declaring_path: String,
22    /// AST node type for this declaration.
23    pub node_type: String,
24    /// LSP completion kind mapped from the AST node type.
25    pub kind: CompletionItemKind,
26}
27
28/// A declaration found within a specific scope.
29#[derive(Debug, Clone)]
30pub struct ScopedDeclaration {
31    /// Variable/function/type name.
32    pub name: String,
33    /// typeIdentifier from typeDescriptions (e.g. "t_struct$_PoolKey_$8887_memory_ptr").
34    pub type_id: String,
35    /// `visibility: private`. Private members are not visible in derived
36    /// contracts, so lookups through `linearized_base_contracts` skip them.
37    pub is_private: bool,
38    /// typeString, shown as the completion item's detail (e.g. "uint256").
39    pub type_string: Option<String>,
40    /// Byte offset of a local declared inside a block, `for` header, or
41    /// `try`/`catch` clause. Such a local is only visible after this point;
42    /// `None` for parameters, state variables, and functions.
43    pub declared_at: Option<usize>,
44}
45
46/// A byte range identifying a scope-creating AST node.
47#[derive(Debug, Clone)]
48pub struct ScopeRange {
49    /// AST node id of this scope.
50    pub node_id: NodeId,
51    /// Byte offset where this scope starts (from `src` field).
52    pub start: usize,
53    /// Byte offset where this scope ends (start + length).
54    pub end: usize,
55    /// Source file id (from `src` field).
56    pub file_id: FileId,
57}
58
59/// Completion cache built from the AST.
60#[derive(Debug)]
61pub struct CompletionCache {
62    /// Named identifiers offered as non-dot completions.
63    /// Struct fields, parameters, and locals are omitted. Parameters and locals
64    /// are added for the cursor's scope in `append_scoped_variables`.
65    pub names: Vec<CompletionItem>,
66
67    /// name → typeIdentifier (for dot-completion: look up what type a variable is).
68    pub name_to_type: HashMap<SymbolName, TypeIdentifier>,
69
70    /// node id → Vec<CompletionItem> (members of structs, contracts, enums, libraries).
71    pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
72
73    /// typeIdentifier → node id (resolve a type string to its definition).
74    pub type_to_node: HashMap<TypeIdentifier, NodeId>,
75
76    /// contract/library/interface name → node id (for direct name dot-completion like `FullMath.`).
77    pub name_to_node_id: HashMap<SymbolName, NodeId>,
78
79    /// (container contract/library/interface node id, member name) → node id of an enum
80    /// declared inside it, for `Lib.Enum.` chains. solc gives declarations no
81    /// `typeDescriptions`, so the member can't be resolved through `name_to_type`.
82    pub nested_types: HashMap<(NodeId, SymbolName), NodeId>,
83
84    /// node id → Vec<CompletionItem> from methodIdentifiers in .contracts section.
85    /// Full function signatures with 4-byte selectors for contracts/interfaces.
86    pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
87
88    /// (contract_node_id, fn_name) → return typeIdentifier.
89    /// For resolving `foo().` — look up what `foo` returns.
90    pub function_return_types: HashMap<(NodeId, String), String>,
91
92    /// typeIdentifier → Vec<CompletionItem> from UsingForDirective.
93    /// Library functions available on a type via `using X for Y`.
94    pub using_for: HashMap<TypeIdentifier, Vec<CompletionItem>>,
95
96    /// Wildcard using-for: `using X for *` — available on all types.
97    pub using_for_wildcard: Vec<CompletionItem>,
98
99    /// Pre-built general completions (AST names + keywords + globals + units).
100    /// Built once, returned by reference on every non-dot completion request.
101    pub general_completions: Vec<CompletionItem>,
102
103    /// Labels in `general_completions`, so scoped parameters and locals can be
104    /// deduplicated against it without rehashing the list on every request.
105    pub general_labels: HashSet<String>,
106
107    /// Parameters and locals as flat items (first declaration per name). Offered
108    /// only when the cursor's scope cannot be resolved, e.g. no file id.
109    pub unscoped_variables: Vec<CompletionItem>,
110
111    /// scope node_id → declarations in that scope.
112    /// Each scope (Block, FunctionDefinition, ContractDefinition, SourceUnit)
113    /// has the variables/functions/types declared directly within it.
114    pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
115
116    /// node_id → parent scope node_id.
117    /// Walk this chain upward to widen the search scope.
118    pub scope_parent: HashMap<NodeId, NodeId>,
119
120    /// All scope ranges, for finding which scope a byte position falls in.
121    /// Sorted by span size ascending (smallest first) for efficient innermost-scope lookup.
122    pub scope_ranges: Vec<ScopeRange>,
123
124    /// absolute file path → AST source file id.
125    /// Used to map a URI to the file_id needed for scope resolution.
126    pub path_to_file_id: HashMap<RelPath, FileId>,
127
128    /// contract node_id → linearized base contracts (C3 linearization order).
129    /// First element is the contract itself, followed by parents in resolution order.
130    /// Used to search inherited state variables and functions during scope resolution.
131    pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
132
133    /// contract/interface/library node_id → contractKind string.
134    /// Values are `"contract"`, `"interface"`, or `"library"`.
135    /// Used to determine which `type(X).` members to offer.
136    pub contract_kinds: HashMap<NodeId, String>,
137
138    /// Type name → members reachable only through that name, never through a
139    /// value of the type: `Currency.wrap` / `Currency.unwrap` for a user-defined
140    /// value type. Kept out of `node_members`, which also serves values.
141    pub type_name_members: HashMap<SymbolName, Vec<CompletionItem>>,
142
143    /// Directly-declared importable top-level symbols keyed by symbol name.
144    ///
145    /// This intentionally excludes imported aliases/re-exports and excludes
146    /// non-constant variables. It is used for import-on-completion candidate
147    /// lookup without re-scanning the full AST per request.
148    pub top_level_importables_by_name: HashMap<SymbolName, Vec<TopLevelImportable>>,
149
150    /// Directly-declared importable top-level symbols keyed by declaring file path.
151    ///
152    /// This enables cheap incremental invalidation/update on file edits/deletes:
153    /// only the changed file's symbols need to be replaced.
154    pub top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>>,
155}
156
157/// Map AST nodeType to LSP CompletionItemKind.
158fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
159    match node_type {
160        "FunctionDefinition" => CompletionItemKind::FUNCTION,
161        "VariableDeclaration" => CompletionItemKind::VARIABLE,
162        "ContractDefinition" => CompletionItemKind::CLASS,
163        "StructDefinition" => CompletionItemKind::STRUCT,
164        "EnumDefinition" => CompletionItemKind::ENUM,
165        "EnumValue" => CompletionItemKind::ENUM_MEMBER,
166        "EventDefinition" => CompletionItemKind::EVENT,
167        "ErrorDefinition" => CompletionItemKind::EVENT,
168        "ModifierDefinition" => CompletionItemKind::METHOD,
169        "ImportDirective" => CompletionItemKind::MODULE,
170        _ => CompletionItemKind::TEXT,
171    }
172}
173
174/// Parse the `src` field of an AST node: "offset:length:fileId".
175/// Returns the parsed SourceLoc or None if the format is invalid.
176fn parse_src(node: &Value) -> Option<SourceLoc> {
177    let src = node.get("src").and_then(|v| v.as_str())?;
178    SourceLoc::parse(src)
179}
180
181/// Extract the trailing node id from a typeIdentifier string.
182/// e.g. `t_struct$_PoolKey_$8887_storage_ptr` → Some(8887)
183///      `t_contract$_IHooks_$2248` → Some(2248)
184///      `t_uint256` → None
185pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
186    // Pattern: ..._$<digits>... where digits follow the last _$
187    // We find all _$<digits> groups and take the last one that's part of the type name
188    let mut last_id = None;
189    let mut i = 0;
190    let bytes = type_id.as_bytes();
191    while i < bytes.len() {
192        if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
193            i += 2;
194            let start = i;
195            while i < bytes.len() && bytes[i].is_ascii_digit() {
196                i += 1;
197            }
198            if i > start
199                && let Ok(id) = type_id[start..i].parse::<i64>()
200            {
201                last_id = Some(NodeId(id));
202            }
203        } else {
204            i += 1;
205        }
206    }
207    last_id
208}
209
210/// Extract the deepest value type from a mapping typeIdentifier.
211/// Peels off all `t_mapping$_<key>_$_<value>` layers and returns the innermost value type.
212///
213/// e.g. `t_mapping$_t_address_$_t_uint256_$` → `t_uint256`
214///      `t_mapping$_t_address_$_t_mapping$_t_uint256_$_t_uint256_$_$` → `t_uint256`
215///      `t_mapping$_t_userDefinedValueType$_PoolId_$8841_$_t_struct$_State_$4809_storage_$` → `t_struct$_State_$4809_storage`
216pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
217    let mut current = type_id;
218
219    loop {
220        if !current.starts_with("t_mapping$_") {
221            // Not a mapping — this is the value type
222            // Strip trailing _$ suffixes (mapping closers)
223            let result = current.trim_end_matches("_$");
224            return if result.is_empty() {
225                None
226            } else {
227                Some(result.to_string())
228            };
229        }
230
231        // Strip "t_mapping$_" prefix to get "<key>_$_<value>_$"
232        let inner = &current["t_mapping$_".len()..];
233
234        // Find the boundary between key and value.
235        // We need to find the _$_ that separates key from value at depth 0.
236        // Each $_ opens a nesting level, each _$ closes one.
237        let mut depth = 0i32;
238        let bytes = inner.as_bytes();
239        let mut split_pos = None;
240
241        let mut i = 0;
242        while i < bytes.len() {
243            if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
244                depth += 1;
245                i += 2;
246            } else if i + 2 < bytes.len()
247                && bytes[i] == b'_'
248                && bytes[i + 1] == b'$'
249                && bytes[i + 2] == b'_'
250                && depth == 0
251            {
252                // This is the _$_ separator at depth 0
253                split_pos = Some(i);
254                break;
255            } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
256                depth -= 1;
257                i += 2;
258            } else {
259                i += 1;
260            }
261        }
262
263        if let Some(pos) = split_pos {
264            // Value type starts after "_$_"
265            current = &inner[pos + 3..];
266        } else {
267            return None;
268        }
269    }
270}
271
272/// Count parameters in an ABI method signature like "swap((address,address),uint256,bytes)".
273/// Counts commas at depth 0 (inside the outer parens), handling nested tuples.
274fn count_abi_params(signature: &str) -> usize {
275    // Find the first '(' and work from there
276    let start = match signature.find('(') {
277        Some(i) => i + 1,
278        None => return 0,
279    };
280    let bytes = signature.as_bytes();
281    if start >= bytes.len() {
282        return 0;
283    }
284    // Check for empty params "()"
285    if bytes[start] == b')' {
286        return 0;
287    }
288    let mut count = 1; // at least one param if not empty
289    let mut depth = 0;
290    for &b in &bytes[start..] {
291        match b {
292            b'(' => depth += 1,
293            b')' => {
294                if depth == 0 {
295                    break;
296                }
297                depth -= 1;
298            }
299            b',' if depth == 0 => count += 1,
300            _ => {}
301        }
302    }
303    count
304}
305
306/// Count parameters in an AST-derived signature like "swap(PoolKey key, SwapParams params, bytes hookData)".
307fn count_signature_params(sig: &str) -> usize {
308    count_abi_params(sig)
309}
310
311fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
312    match node_type {
313        "ContractDefinition"
314        | "StructDefinition"
315        | "EnumDefinition"
316        | "UserDefinedValueTypeDefinition"
317        | "FunctionDefinition" => true,
318        "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
319        _ => false,
320    }
321}
322
323fn build_top_level_importables_by_name(
324    by_file: &HashMap<RelPath, Vec<TopLevelImportable>>,
325) -> HashMap<SymbolName, Vec<TopLevelImportable>> {
326    let mut by_name: HashMap<SymbolName, Vec<TopLevelImportable>> = HashMap::new();
327    for symbols in by_file.values() {
328        for symbol in symbols {
329            by_name
330                .entry(SymbolName::new(symbol.name.clone()))
331                .or_default()
332                .push(symbol.clone());
333        }
334    }
335    by_name
336}
337
338/// Extract directly-declared importable top-level symbols from a file AST.
339///
340/// - Includes: contract/interface/library/struct/enum/UDVT/top-level free function/top-level constant
341/// - Excludes: imported aliases/re-exports, nested declarations, non-constant variables
342pub fn extract_top_level_importables_for_file(path: &str, ast: &Value) -> Vec<TopLevelImportable> {
343    let mut out: Vec<TopLevelImportable> = Vec::new();
344    let mut stack: Vec<&Value> = vec![ast];
345    let mut source_unit_id: Option<NodeId> = None;
346
347    while let Some(tree) = stack.pop() {
348        let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
349        let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
350        if node_type == "SourceUnit" {
351            source_unit_id = node_id;
352        }
353        let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
354
355        if !name.is_empty()
356            && is_top_level_importable_decl(node_type, tree)
357            && let Some(src_scope) = source_unit_id
358            && tree.get("scope").and_then(|v| v.as_i64()) == Some(src_scope.0)
359        {
360            out.push(TopLevelImportable {
361                name: name.to_string(),
362                declaring_path: path.to_string(),
363                node_type: node_type.to_string(),
364                kind: node_type_to_completion_kind(node_type),
365            });
366        }
367
368        for key in CHILD_KEYS {
369            push_if_node_or_array(tree, key, &mut stack);
370        }
371    }
372
373    out
374}
375
376impl CompletionCache {
377    /// Replace top-level importables for a file path and rebuild the by-name index.
378    /// Pass an empty `symbols` list when the file is deleted.
379    pub fn replace_top_level_importables_for_path(
380        &mut self,
381        path: String,
382        symbols: Vec<TopLevelImportable>,
383    ) {
384        self.top_level_importables_by_file
385            .insert(RelPath::new(path), symbols);
386        self.top_level_importables_by_name =
387            build_top_level_importables_by_name(&self.top_level_importables_by_file);
388    }
389}
390
391/// Build a CompletionCache from AST sources and contracts.
392/// `contracts` is the `.contracts` section of the compiler output (optional).
393///
394/// When `file_id_remap` is provided, every solc-assigned file ID (both in
395/// `path_to_file_id` and in `ScopeRange.file_id`) is translated into a
396/// canonical ID from the project-wide [`PathInterner`].  Pass `None` in
397/// tests or when canonical IDs are not needed.
398pub fn build_completion_cache(
399    sources: &Value,
400    contracts: Option<&Value>,
401    file_id_remap: Option<&HashMap<u64, FileId>>,
402) -> CompletionCache {
403    let source_count = sources.as_object().map_or(0, |obj| obj.len());
404    // Pre-size collections based on source count to reduce rehash churn.
405    // Estimates: ~20 names/file, ~5 contracts/file, ~10 functions/file.
406    let est_names = source_count * 20;
407    let est_contracts = source_count * 5;
408
409    let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
410    let mut seen_names: HashMap<SymbolName, usize> = HashMap::with_capacity(est_names);
411    let mut name_to_type: HashMap<SymbolName, TypeIdentifier> = HashMap::with_capacity(est_names);
412    let mut nested_types: HashMap<(NodeId, SymbolName), NodeId> = HashMap::new();
413    let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
414        HashMap::with_capacity(est_contracts);
415    let mut type_to_node: HashMap<TypeIdentifier, NodeId> = HashMap::with_capacity(est_contracts);
416    let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
417        HashMap::with_capacity(est_contracts);
418    let mut name_to_node_id: HashMap<SymbolName, NodeId> = HashMap::with_capacity(est_names);
419    let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
420    let mut type_name_members: HashMap<SymbolName, Vec<CompletionItem>> = HashMap::new();
421
422    // Collect (path, contract_name, node_id) during AST walk for methodIdentifiers lookup after.
423    let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
424
425    // contract_node_id → fn_name → Vec<signature> (for matching method_identifiers to AST signatures)
426    let mut function_signatures: HashMap<NodeId, HashMap<SymbolName, Vec<String>>> =
427        HashMap::with_capacity(est_contracts);
428
429    // (contract_node_id, fn_name) → return typeIdentifier
430    let mut function_return_types: HashMap<(NodeId, String), String> =
431        HashMap::with_capacity(source_count * 10);
432
433    // typeIdentifier → Vec<CompletionItem> from UsingForDirective
434    let mut using_for: HashMap<TypeIdentifier, Vec<CompletionItem>> =
435        HashMap::with_capacity(source_count);
436    let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
437
438    // Temp: (library_node_id, target_type_id_or_none) for resolving after walk
439    let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
440
441    // Scope-aware completion data
442    let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
443        HashMap::with_capacity(est_contracts);
444    let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
445    let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
446    let mut path_to_file_id: HashMap<RelPath, FileId> = HashMap::with_capacity(source_count);
447    let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
448        HashMap::with_capacity(est_contracts);
449    let mut top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>> =
450        HashMap::with_capacity(est_names);
451    // Node type of every scope a variable can declare into, recorded as the
452    // walk reaches it (always before the declarations it contains). It tells a
453    // file-level or state variable (SourceUnit/ContractDefinition scope) from
454    // a parameter (function/modifier) or a local (block, `for`, `try` clause).
455    // Struct fields and event/error parameters have scopes not recorded here.
456    let mut var_scope_kinds: HashMap<NodeId, &str> = HashMap::new();
457    let mut unscoped_variables: Vec<CompletionItem> = Vec::new();
458    let mut seen_unscoped: HashSet<String> = HashSet::new();
459
460    if let Some(sources_obj) = sources.as_object() {
461        for (path, source_data) in sources_obj {
462            if let Some(ast) = source_data.get("ast") {
463                // Map file path → source file id for scope resolution.
464                // When a canonical remap is available, translate solc's raw
465                // file ID into the project-wide canonical ID.
466                if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
467                    let canonical_fid = file_id_remap
468                        .and_then(|r| r.get(&fid).copied())
469                        .unwrap_or(FileId(fid));
470                    path_to_file_id.insert(RelPath::new(path), canonical_fid);
471                }
472                let file_importables = extract_top_level_importables_for_file(path, ast);
473                if !file_importables.is_empty() {
474                    top_level_importables_by_file.insert(RelPath::new(path), file_importables);
475                }
476                let mut stack: Vec<&Value> = vec![ast];
477
478                while let Some(tree) = stack.pop() {
479                    let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
480                    let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
481                    let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
482
483                    // --- Scope-aware data collection ---
484
485                    // Record scope-creating nodes and their byte ranges. These are
486                    // every node type solc uses as a VariableDeclaration's `scope`:
487                    // a `for` init variable is scoped to its ForStatement, and
488                    // `try ... returns (x)` / `catch (bytes memory d)` variables to
489                    // their TryCatchClause.
490                    let is_scope_node = matches!(
491                        node_type,
492                        "SourceUnit"
493                            | "ContractDefinition"
494                            | "FunctionDefinition"
495                            | "ModifierDefinition"
496                            | "Block"
497                            | "UncheckedBlock"
498                            | "ForStatement"
499                            | "TryCatchClause"
500                    );
501                    if is_scope_node && let Some(nid) = node_id {
502                        if let Some(src_loc) = parse_src(tree) {
503                            let canonical_fid = file_id_remap
504                                .and_then(|r| r.get(&src_loc.file_id.0).copied())
505                                .unwrap_or(src_loc.file_id);
506                            scope_ranges.push(ScopeRange {
507                                node_id: nid,
508                                start: src_loc.offset,
509                                end: src_loc.end(),
510                                file_id: canonical_fid,
511                            });
512                        }
513                        // Record parent link: this node's scope → its parent
514                        if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_i64()) {
515                            scope_parent.insert(nid, NodeId(parent_id));
516                        }
517                    }
518
519                    // For ContractDefinitions, record linearizedBaseContracts
520                    if node_type == "ContractDefinition"
521                        && let Some(nid) = node_id
522                        && let Some(bases) = tree
523                            .get("linearizedBaseContracts")
524                            .and_then(|v| v.as_array())
525                    {
526                        let base_ids: Vec<NodeId> = bases
527                            .iter()
528                            .filter_map(|b| b.as_i64())
529                            .map(NodeId)
530                            .collect();
531                        if !base_ids.is_empty() {
532                            linearized_base_contracts.insert(nid, base_ids);
533                        }
534                    }
535
536                    // For VariableDeclarations, record the declaration in its scope
537                    if node_type == "VariableDeclaration"
538                        && !name.is_empty()
539                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
540                        && let Some(tid) = tree
541                            .get("typeDescriptions")
542                            .and_then(|td| td.get("typeIdentifier"))
543                            .and_then(|v| v.as_str())
544                    {
545                        scope_declarations
546                            .entry(NodeId(scope_raw))
547                            .or_default()
548                            .push(ScopedDeclaration {
549                                name: name.to_string(),
550                                type_id: tid.to_string(),
551                                is_private: tree.get("visibility").and_then(|v| v.as_str())
552                                    == Some("private"),
553                                type_string: type_string_of(tree),
554                                declared_at: var_scope_kinds
555                                    .get(&NodeId(scope_raw))
556                                    .filter(|k| is_statement_scope(k))
557                                    .and(parse_src(tree))
558                                    .map(|loc| loc.offset),
559                            });
560                    }
561
562                    // For FunctionDefinitions, record them in their parent scope (the contract)
563                    if node_type == "FunctionDefinition"
564                        && !name.is_empty()
565                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
566                        && let Some(tid) = tree
567                            .get("typeDescriptions")
568                            .and_then(|td| td.get("typeIdentifier"))
569                            .and_then(|v| v.as_str())
570                    {
571                        scope_declarations
572                            .entry(NodeId(scope_raw))
573                            .or_default()
574                            .push(ScopedDeclaration {
575                                name: name.to_string(),
576                                type_id: tid.to_string(),
577                                is_private: tree.get("visibility").and_then(|v| v.as_str())
578                                    == Some("private"),
579                                type_string: type_string_of(tree),
580                                declared_at: None,
581                            });
582                    }
583
584                    if (matches!(
585                        node_type,
586                        "SourceUnit"
587                            | "ContractDefinition"
588                            | "FunctionDefinition"
589                            | "ModifierDefinition"
590                    ) || is_statement_scope(node_type))
591                        && let Some(id) = node_id
592                    {
593                        var_scope_kinds.insert(id, node_type);
594                    }
595
596                    // Only declarations that can be written as bare names belong in
597                    // the non-dot list. Identifier / MemberAccess / Yul names are
598                    // uses. Struct fields, function and event parameters, and
599                    // locals are not global names; parameters and locals are added
600                    // back at request time when the cursor is in their scope.
601                    let var_scope_kind = if node_type == "VariableDeclaration" {
602                        tree.get("scope")
603                            .and_then(|v| v.as_i64())
604                            .map(|scope| var_scope_kinds.get(&NodeId(scope)).copied())
605                    } else {
606                        None
607                    };
608                    let is_hidden_variable = var_scope_kind.is_some_and(|kind| {
609                        !matches!(kind, Some("SourceUnit" | "ContractDefinition"))
610                    });
611
612                    // Parameters and locals, kept as a flat fallback for requests
613                    // whose scope cannot be resolved.
614                    if !name.is_empty()
615                        && var_scope_kind.flatten().is_some_and(|kind| {
616                            !matches!(kind, "SourceUnit" | "ContractDefinition")
617                        })
618                        && seen_unscoped.insert(name.to_string())
619                    {
620                        unscoped_variables.push(CompletionItem {
621                            label: name.to_string(),
622                            kind: Some(CompletionItemKind::VARIABLE),
623                            detail: type_string_of(tree),
624                            ..Default::default()
625                        });
626                    }
627                    let is_bare_name_decl = matches!(
628                        node_type,
629                        "ContractDefinition"
630                            | "StructDefinition"
631                            | "EnumDefinition"
632                            | "UserDefinedValueTypeDefinition"
633                            | "FunctionDefinition"
634                            | "ModifierDefinition"
635                            | "EventDefinition"
636                            | "ErrorDefinition"
637                            | "VariableDeclaration"
638                            | "ImportDirective"
639                    );
640
641                    // Collect named nodes as completion items
642                    if !name.is_empty()
643                        && is_bare_name_decl
644                        && !is_hidden_variable
645                        && !seen_names.contains_key(name)
646                    {
647                        let type_string = tree
648                            .get("typeDescriptions")
649                            .and_then(|td| td.get("typeString"))
650                            .and_then(|v| v.as_str())
651                            .map(|s| s.to_string());
652
653                        let kind = node_type_to_completion_kind(node_type);
654
655                        let item = CompletionItem {
656                            label: name.to_string(),
657                            kind: Some(kind),
658                            detail: type_string,
659                            ..Default::default()
660                        };
661
662                        let idx = names.len();
663                        names.push(item);
664                        seen_names.insert(SymbolName::new(name), idx);
665                    }
666
667                    // name → typeIdentifier for every named node, including the
668                    // struct fields, parameters, and locals left out of `names`:
669                    // chain resolution looks member types up here (`a.field.`),
670                    // and it is the fallback when the cursor's scope is unknown.
671                    // `t_type$...` is a use of a type name as an expression (the
672                    // `IHooks` in `IHooks(addr)`); recording it would shadow the
673                    // `name_to_node_id` lookup that resolves type names.
674                    if !name.is_empty()
675                        && !name_to_type.contains_key(name)
676                        && let Some(tid) = tree
677                            .get("typeDescriptions")
678                            .and_then(|td| td.get("typeIdentifier"))
679                            .and_then(|v| v.as_str())
680                        && !tid.starts_with("t_type$")
681                    {
682                        name_to_type.insert(SymbolName::new(name), TypeIdentifier::new(tid));
683                    }
684
685                    // Import aliases (`import {Test as MyTest}`, `import "x" as X`)
686                    // are only named on uses, which `names` no longer collects.
687                    if node_type == "ImportDirective" {
688                        let symbol_aliases = tree
689                            .get("symbolAliases")
690                            .and_then(|v| v.as_array())
691                            .into_iter()
692                            .flatten()
693                            .filter_map(|a| a.get("local").and_then(|v| v.as_str()));
694                        let unit_alias = tree.get("unitAlias").and_then(|v| v.as_str());
695                        for alias in symbol_aliases.chain(unit_alias) {
696                            if alias.is_empty() || seen_names.contains_key(alias) {
697                                continue;
698                            }
699                            seen_names.insert(SymbolName::new(alias), names.len());
700                            names.push(CompletionItem {
701                                label: alias.to_string(),
702                                kind: Some(CompletionItemKind::MODULE),
703                                ..Default::default()
704                            });
705                        }
706                    }
707
708                    // Collect struct members
709                    if node_type == "StructDefinition"
710                        && let Some(id) = node_id
711                    {
712                        let mut members = Vec::new();
713                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
714                            for member in member_array {
715                                let member_name =
716                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
717                                if member_name.is_empty() {
718                                    continue;
719                                }
720                                let member_type = member
721                                    .get("typeDescriptions")
722                                    .and_then(|td| td.get("typeString"))
723                                    .and_then(|v| v.as_str())
724                                    .map(|s| s.to_string());
725
726                                members.push(CompletionItem {
727                                    label: member_name.to_string(),
728                                    kind: Some(CompletionItemKind::FIELD),
729                                    detail: member_type,
730                                    ..Default::default()
731                                });
732                            }
733                        }
734                        if !members.is_empty() {
735                            node_members.insert(id, members);
736                        }
737
738                        // Map typeIdentifier → node id
739                        if let Some(tid) = tree
740                            .get("typeDescriptions")
741                            .and_then(|td| td.get("typeIdentifier"))
742                            .and_then(|v| v.as_str())
743                        {
744                            type_to_node.insert(TypeIdentifier::new(tid), id);
745                        }
746                    }
747
748                    // Collect contract/library members (functions, state variables, events, etc.)
749                    if node_type == "ContractDefinition"
750                        && let Some(id) = node_id
751                    {
752                        let mut members = Vec::new();
753                        let mut fn_sigs: HashMap<SymbolName, Vec<String>> = HashMap::new();
754                        if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
755                            for member in nodes_array {
756                                let member_type = member
757                                    .get("nodeType")
758                                    .and_then(|v| v.as_str())
759                                    .unwrap_or("");
760                                let member_name =
761                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
762                                if member_name.is_empty() {
763                                    continue;
764                                }
765
766                                if member_type == "EnumDefinition"
767                                    && let Some(member_id) =
768                                        member.get("id").and_then(|v| v.as_i64())
769                                {
770                                    nested_types.insert(
771                                        (id, SymbolName::new(member_name)),
772                                        NodeId(member_id),
773                                    );
774                                }
775
776                                // Build function signature and collect return types for FunctionDefinitions
777                                let (member_detail, label_details) =
778                                    if member_type == "FunctionDefinition" {
779                                        // Collect return type for chain resolution.
780                                        // Only single-return functions can be dot-chained
781                                        // (tuples require destructuring).
782                                        if let Some(ret_params) = member
783                                            .get("returnParameters")
784                                            .and_then(|rp| rp.get("parameters"))
785                                            .and_then(|v| v.as_array())
786                                            && ret_params.len() == 1
787                                            && let Some(ret_tid) = ret_params[0]
788                                                .get("typeDescriptions")
789                                                .and_then(|td| td.get("typeIdentifier"))
790                                                .and_then(|v| v.as_str())
791                                        {
792                                            function_return_types.insert(
793                                                (id, member_name.to_string()),
794                                                ret_tid.to_string(),
795                                            );
796                                        }
797
798                                        if let Some(sig) = build_function_signature(member) {
799                                            fn_sigs
800                                                .entry(SymbolName::new(member_name))
801                                                .or_default()
802                                                .push(sig.clone());
803                                            (Some(sig), None)
804                                        } else {
805                                            (
806                                                member
807                                                    .get("typeDescriptions")
808                                                    .and_then(|td| td.get("typeString"))
809                                                    .and_then(|v| v.as_str())
810                                                    .map(|s| s.to_string()),
811                                                None,
812                                            )
813                                        }
814                                    } else {
815                                        (
816                                            member
817                                                .get("typeDescriptions")
818                                                .and_then(|td| td.get("typeString"))
819                                                .and_then(|v| v.as_str())
820                                                .map(|s| s.to_string()),
821                                            None,
822                                        )
823                                    };
824
825                                let kind = node_type_to_completion_kind(member_type);
826                                members.push(CompletionItem {
827                                    label: member_name.to_string(),
828                                    kind: Some(kind),
829                                    detail: member_detail,
830                                    label_details,
831                                    ..Default::default()
832                                });
833                            }
834                        }
835                        if !members.is_empty() {
836                            node_members.insert(id, members);
837                        }
838                        if !fn_sigs.is_empty() {
839                            function_signatures.insert(id, fn_sigs);
840                        }
841
842                        if let Some(tid) = tree
843                            .get("typeDescriptions")
844                            .and_then(|td| td.get("typeIdentifier"))
845                            .and_then(|v| v.as_str())
846                        {
847                            type_to_node.insert(TypeIdentifier::new(tid), id);
848                        }
849
850                        // Record for methodIdentifiers lookup after traversal
851                        if !name.is_empty() {
852                            contract_locations.push((path.clone(), name.to_string(), id));
853                            name_to_node_id.insert(SymbolName::new(name), id);
854                        }
855
856                        // Record contractKind (contract, interface, library) for type(X). completions
857                        if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
858                            contract_kinds.insert(id, ck.to_string());
859                        }
860                    }
861
862                    // Collect enum members
863                    if node_type == "EnumDefinition"
864                        && let Some(id) = node_id
865                    {
866                        let mut members = Vec::new();
867                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
868                            for member in member_array {
869                                let member_name =
870                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
871                                if member_name.is_empty() {
872                                    continue;
873                                }
874                                members.push(CompletionItem {
875                                    label: member_name.to_string(),
876                                    kind: Some(CompletionItemKind::ENUM_MEMBER),
877                                    detail: None,
878                                    ..Default::default()
879                                });
880                            }
881                        }
882                        if !members.is_empty() {
883                            node_members.insert(id, members);
884                        }
885
886                        if let Some(tid) = tree
887                            .get("typeDescriptions")
888                            .and_then(|td| td.get("typeIdentifier"))
889                            .and_then(|v| v.as_str())
890                        {
891                            type_to_node.insert(TypeIdentifier::new(tid), id);
892                        }
893                    }
894
895                    // User-defined value types: `Name.wrap(underlying)` / `Name.unwrap(value)`
896                    if node_type == "UserDefinedValueTypeDefinition" && !name.is_empty() {
897                        let underlying = tree
898                            .get("underlyingType")
899                            .and_then(|t| t.get("typeDescriptions"))
900                            .and_then(|td| td.get("typeString"))
901                            .and_then(|v| v.as_str())
902                            .unwrap_or("");
903                        type_name_members.insert(
904                            SymbolName::new(name),
905                            vec![
906                                builtin_member(
907                                    &format!("wrap({underlying})"),
908                                    CompletionItemKind::FUNCTION,
909                                    Some(name),
910                                ),
911                                builtin_member(
912                                    &format!("unwrap({name})"),
913                                    CompletionItemKind::FUNCTION,
914                                    Some(underlying),
915                                ),
916                            ],
917                        );
918                    }
919
920                    // Collect UsingForDirective: using Library for Type
921                    if node_type == "UsingForDirective" {
922                        // Get target type (None = wildcard `for *`)
923                        let target_type = tree.get("typeName").and_then(|tn| {
924                            tn.get("typeDescriptions")
925                                .and_then(|td| td.get("typeIdentifier"))
926                                .and_then(|v| v.as_str())
927                                .map(|s| s.to_string())
928                        });
929
930                        // Form 1: library name object with referencedDeclaration
931                        if let Some(lib) = tree.get("libraryName") {
932                            if let Some(lib_id) =
933                                lib.get("referencedDeclaration").and_then(|v| v.as_i64())
934                            {
935                                using_for_directives.push((NodeId(lib_id), target_type));
936                            }
937                        }
938                        // Form 2: functionList array — individual function references
939                        // These are typically operator overloads (not dot-callable),
940                        // but collect non-operator ones just in case
941                        else if let Some(func_list) =
942                            tree.get("functionList").and_then(|v| v.as_array())
943                        {
944                            for entry in func_list {
945                                // Skip operator overloads
946                                if entry.get("operator").is_some() {
947                                    continue;
948                                }
949                                if let Some(def) = entry.get("definition") {
950                                    let fn_name =
951                                        def.get("name").and_then(|v| v.as_str()).unwrap_or("");
952                                    if !fn_name.is_empty() {
953                                        let items = if let Some(ref tid) = target_type {
954                                            using_for
955                                                .entry(TypeIdentifier::new(tid.clone()))
956                                                .or_default()
957                                        } else {
958                                            &mut using_for_wildcard
959                                        };
960                                        items.push(CompletionItem {
961                                            label: fn_name.to_string(),
962                                            kind: Some(CompletionItemKind::FUNCTION),
963                                            detail: None,
964                                            ..Default::default()
965                                        });
966                                    }
967                                }
968                            }
969                        }
970                    }
971
972                    // Traverse children
973                    for key in CHILD_KEYS {
974                        push_if_node_or_array(tree, key, &mut stack);
975                    }
976                }
977            }
978        }
979    }
980
981    // Resolve UsingForDirective library references (Form 1)
982    // Now that node_members is populated, look up each library's functions
983    for (lib_id, target_type) in &using_for_directives {
984        if let Some(lib_members) = node_members.get(lib_id) {
985            let items: Vec<CompletionItem> = lib_members
986                .iter()
987                .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
988                .cloned()
989                .collect();
990            if !items.is_empty() {
991                if let Some(tid) = target_type {
992                    using_for
993                        .entry(TypeIdentifier::new(tid.clone()))
994                        .or_default()
995                        .extend(items);
996                } else {
997                    using_for_wildcard.extend(items);
998                }
999            }
1000        }
1001    }
1002
1003    // Build method_identifiers from .contracts section
1004    if let Some(contracts_val) = contracts
1005        && let Some(contracts_obj) = contracts_val.as_object()
1006    {
1007        for (path, contract_name, node_id) in &contract_locations {
1008            // Get AST function signatures for this contract (if available)
1009            let fn_sigs = function_signatures.get(node_id);
1010
1011            if let Some(path_entry) = contracts_obj.get(path)
1012                && let Some(contract_entry) = path_entry.get(contract_name)
1013                && let Some(evm) = contract_entry.get("evm")
1014                && let Some(methods) = evm.get("methodIdentifiers")
1015                && let Some(methods_obj) = methods.as_object()
1016            {
1017                let mut items: Vec<CompletionItem> = Vec::new();
1018                for (signature, selector_val) in methods_obj {
1019                    // signature is e.g. "swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)"
1020                    // selector_val is e.g. "f3cd914c"
1021                    let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
1022                    let selector_str = selector_val
1023                        .as_str()
1024                        .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
1025                        .unwrap_or_default();
1026
1027                    // Look up the AST signature with parameter names
1028                    let description =
1029                        fn_sigs
1030                            .and_then(|sigs| sigs.get(&fn_name))
1031                            .and_then(|sig_list| {
1032                                if sig_list.len() == 1 {
1033                                    // Only one overload — use it directly
1034                                    Some(sig_list[0].clone())
1035                                } else {
1036                                    // Multiple overloads — match by parameter count
1037                                    let abi_param_count = count_abi_params(signature);
1038                                    sig_list
1039                                        .iter()
1040                                        .find(|s| count_signature_params(s) == abi_param_count)
1041                                        .cloned()
1042                                }
1043                            });
1044
1045                    // The selector goes in `documentation`, not `labelDetails.detail`:
1046                    // editors render labelDetails.detail flush against the label
1047                    // (`approve0x095ea7b3`), which clutters the completion menu.
1048                    items.push(CompletionItem {
1049                        label: fn_name,
1050                        kind: Some(CompletionItemKind::FUNCTION),
1051                        detail: Some(signature.clone()),
1052                        label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
1053                            detail: None,
1054                            description,
1055                        }),
1056                        documentation: (!selector_str.is_empty()).then(|| {
1057                            tower_lsp::lsp_types::Documentation::String(format!(
1058                                "selector: {selector_str}"
1059                            ))
1060                        }),
1061                        ..Default::default()
1062                    });
1063                }
1064                if !items.is_empty() {
1065                    method_identifiers.insert(*node_id, items);
1066                }
1067            }
1068        }
1069    }
1070
1071    // Pre-build the general completions list (names + statics) once
1072    let mut general_completions = names.clone();
1073    general_completions.extend(get_static_completions());
1074    let general_labels: HashSet<String> = general_completions
1075        .iter()
1076        .map(|item| item.label.clone())
1077        .collect();
1078
1079    // Sort scope_ranges by span size ascending (smallest first) for innermost-scope lookup
1080    scope_ranges.sort_by_key(|r| r.end - r.start);
1081
1082    // Infer parent links for Block/UncheckedBlock/ForStatement/TryCatchClause/ModifierDefinition nodes.
1083    // These AST nodes have no `scope` field, so scope_parent has no entry for them.
1084    // For each orphan, find the next-larger enclosing scope range in the same file.
1085    // scope_ranges is sorted smallest-first, so we scan forward from each orphan's
1086    // position to find the first range that strictly contains it.
1087    let orphan_ids: Vec<NodeId> = scope_ranges
1088        .iter()
1089        .filter(|r| !scope_parent.contains_key(&r.node_id))
1090        .map(|r| r.node_id)
1091        .collect();
1092    // Build a lookup from node_id → (start, end, file_id) for quick access
1093    let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
1094        .iter()
1095        .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
1096        .collect();
1097    for orphan_id in &orphan_ids {
1098        if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
1099            // Find the smallest range that strictly contains this orphan's range
1100            // (same file, starts at or before, ends at or after, and is strictly larger)
1101            let parent = scope_ranges
1102                .iter()
1103                .find(|r| {
1104                    r.node_id != *orphan_id
1105                        && r.file_id == file_id
1106                        && r.start <= start
1107                        && r.end >= end
1108                        && (r.end - r.start) > (end - start)
1109                })
1110                .map(|r| r.node_id);
1111            if let Some(parent_id) = parent {
1112                scope_parent.insert(*orphan_id, parent_id);
1113            }
1114        }
1115    }
1116
1117    let top_level_importables_by_name =
1118        build_top_level_importables_by_name(&top_level_importables_by_file);
1119
1120    CompletionCache {
1121        names,
1122        name_to_type,
1123        nested_types,
1124        node_members,
1125        type_to_node,
1126        name_to_node_id,
1127        method_identifiers,
1128        function_return_types,
1129        using_for,
1130        using_for_wildcard,
1131        general_completions,
1132        general_labels,
1133        unscoped_variables,
1134        scope_declarations,
1135        scope_parent,
1136        scope_ranges,
1137        path_to_file_id,
1138        linearized_base_contracts,
1139        contract_kinds,
1140        type_name_members,
1141        top_level_importables_by_name,
1142        top_level_importables_by_file,
1143    }
1144}
1145
1146/// Scopes whose variables are locals: visible only after their declaration.
1147fn is_statement_scope(node_type: &str) -> bool {
1148    matches!(
1149        node_type,
1150        "Block" | "UncheckedBlock" | "ForStatement" | "TryCatchClause"
1151    )
1152}
1153
1154fn type_string_of(node: &Value) -> Option<String> {
1155    node.get("typeDescriptions")
1156        .and_then(|td| td.get("typeString"))
1157        .and_then(|v| v.as_str())
1158        .map(str::to_string)
1159}
1160
1161/// Magic type member definitions (msg, block, tx, abi, address).
1162fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
1163    let items = match name {
1164        "msg" => vec![
1165            ("data", "bytes calldata"),
1166            ("sender", "address"),
1167            ("sig", "bytes4"),
1168            ("value", "uint256"),
1169        ],
1170        "block" => vec![
1171            ("basefee", "uint256"),
1172            ("blobbasefee", "uint256"),
1173            ("chainid", "uint256"),
1174            ("coinbase", "address payable"),
1175            ("difficulty", "uint256"),
1176            ("gaslimit", "uint256"),
1177            ("number", "uint256"),
1178            ("prevrandao", "uint256"),
1179            ("timestamp", "uint256"),
1180        ],
1181        "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
1182        "abi" => vec![
1183            ("decode(bytes memory, (...))", "..."),
1184            ("encode(...)", "bytes memory"),
1185            ("encodePacked(...)", "bytes memory"),
1186            ("encodeWithSelector(bytes4, ...)", "bytes memory"),
1187            ("encodeWithSignature(string memory, ...)", "bytes memory"),
1188            ("encodeCall(function, (...))", "bytes memory"),
1189        ],
1190        // type(X) — contract type properties
1191        // Also includes interface (interfaceId) and integer (min, max) properties
1192        "type" => vec![
1193            ("name", "string"),
1194            ("creationCode", "bytes memory"),
1195            ("runtimeCode", "bytes memory"),
1196            ("interfaceId", "bytes4"),
1197            ("min", "T"),
1198            ("max", "T"),
1199        ],
1200        // bytes and string type-level members
1201        "bytes" => vec![("concat(...)", "bytes memory")],
1202        "string" => vec![("concat(...)", "string memory")],
1203        _ => return None,
1204    };
1205
1206    Some(
1207        items
1208            .into_iter()
1209            .map(|(signature, detail)| {
1210                builtin_member(signature, CompletionItemKind::PROPERTY, Some(detail))
1211            })
1212            .collect(),
1213    )
1214}
1215
1216/// A built-in member item. `signature` is the name, optionally followed by its
1217/// parameter list (`"encode(...)"`). The label is the bare name, so accepting
1218/// the item inserts `encode` and typing `enc` matches it; the parameter list is
1219/// shown beside it through `labelDetails`.
1220fn builtin_member(
1221    signature: &str,
1222    kind: CompletionItemKind,
1223    detail: Option<&str>,
1224) -> CompletionItem {
1225    let (name, params) = match signature.find('(') {
1226        Some(i) => (&signature[..i], Some(signature[i..].to_string())),
1227        None => (signature, None),
1228    };
1229    CompletionItem {
1230        label: name.to_string(),
1231        kind: Some(kind),
1232        detail: detail.filter(|d| !d.is_empty()).map(str::to_string),
1233        label_details: params.map(|p| tower_lsp::lsp_types::CompletionItemLabelDetails {
1234            detail: Some(p),
1235            description: None,
1236        }),
1237        ..Default::default()
1238    }
1239}
1240
1241/// The kind of argument passed to `type(X)`, which determines which
1242/// meta-type members are available.
1243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1244enum TypeMetaKind {
1245    /// `type(SomeContract)` — has `name`, `creationCode`, `runtimeCode`
1246    Contract,
1247    /// `type(SomeInterface)` — has `name`, `interfaceId`
1248    Interface,
1249    /// `type(uint256)` / `type(int8)` — has `min`, `max`
1250    IntegerType,
1251    /// Unknown argument — return all possible members as a fallback
1252    Unknown,
1253}
1254
1255/// Classify the argument of `type(X)` based on the cache.
1256fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
1257    // Check if it's an integer type: int, uint, int8..int256, uint8..uint256
1258    if arg == "int" || arg == "uint" {
1259        return TypeMetaKind::IntegerType;
1260    }
1261    if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1262        && let Ok(n) = suffix.parse::<u16>()
1263        && (8..=256).contains(&n)
1264        && n % 8 == 0
1265    {
1266        return TypeMetaKind::IntegerType;
1267    }
1268
1269    // With a cache, look up the name to determine contract vs interface
1270    if let Some(c) = cache
1271        && let Some(&node_id) = c.name_to_node_id.get(arg)
1272    {
1273        return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1274            Some("interface") => TypeMetaKind::Interface,
1275            Some("library") => TypeMetaKind::Contract, // libraries have name/creationCode/runtimeCode
1276            _ => TypeMetaKind::Contract,
1277        };
1278    }
1279
1280    TypeMetaKind::Unknown
1281}
1282
1283/// Return context-sensitive `type(X).` completions based on what `X` is.
1284fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1285    let kind = match arg {
1286        Some(a) => classify_type_arg(a, cache),
1287        None => TypeMetaKind::Unknown,
1288    };
1289
1290    let items: Vec<(&str, &str)> = match kind {
1291        TypeMetaKind::Contract => vec![
1292            ("name", "string"),
1293            ("creationCode", "bytes memory"),
1294            ("runtimeCode", "bytes memory"),
1295        ],
1296        TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1297        TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1298        TypeMetaKind::Unknown => vec![
1299            ("name", "string"),
1300            ("creationCode", "bytes memory"),
1301            ("runtimeCode", "bytes memory"),
1302            ("interfaceId", "bytes4"),
1303            ("min", "T"),
1304            ("max", "T"),
1305        ],
1306    };
1307
1308    items
1309        .into_iter()
1310        .map(|(label, detail)| CompletionItem {
1311            label: label.to_string(),
1312            kind: Some(CompletionItemKind::PROPERTY),
1313            detail: Some(detail.to_string()),
1314            ..Default::default()
1315        })
1316        .collect()
1317}
1318
1319/// Address type members (available on any address value).
1320fn address_members() -> Vec<CompletionItem> {
1321    [
1322        ("balance", "uint256", CompletionItemKind::PROPERTY),
1323        ("code", "bytes memory", CompletionItemKind::PROPERTY),
1324        ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1325        ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1326        ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1327        (
1328            "call(bytes memory)",
1329            "(bool, bytes memory)",
1330            CompletionItemKind::FUNCTION,
1331        ),
1332        (
1333            "delegatecall(bytes memory)",
1334            "(bool, bytes memory)",
1335            CompletionItemKind::FUNCTION,
1336        ),
1337        (
1338            "staticcall(bytes memory)",
1339            "(bool, bytes memory)",
1340            CompletionItemKind::FUNCTION,
1341        ),
1342    ]
1343    .iter()
1344    .map(|(signature, detail, kind)| builtin_member(signature, *kind, Some(detail)))
1345    .collect()
1346}
1347
1348/// What kind of access precedes the dot.
1349#[derive(Debug, Clone, PartialEq)]
1350pub enum AccessKind {
1351    /// Plain identifier: `foo.`
1352    Plain,
1353    /// Function call: `foo().` or `foo(x, bar()).`
1354    Call,
1355    /// Index/storage access: `foo[key].` or `foo[func()].`
1356    Index,
1357}
1358
1359/// A segment of a dot-expression chain.
1360#[derive(Debug, Clone, PartialEq)]
1361pub struct DotSegment {
1362    pub name: String,
1363    pub kind: AccessKind,
1364    /// For `Call` segments, the raw text inside the parentheses.
1365    /// e.g. `type(uint256).` → `call_args = Some("uint256")`
1366    pub call_args: Option<String>,
1367}
1368
1369/// Skip backwards over a matched bracket pair (parens or square brackets).
1370/// `pos` should point to the closing bracket. Returns the position of the matching
1371/// opening bracket, or 0 if not found.
1372fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1373    let close = bytes[pos];
1374    let open = match close {
1375        b')' => b'(',
1376        b']' => b'[',
1377        _ => return pos,
1378    };
1379    let mut depth = 1u32;
1380    let mut i = pos;
1381    while i > 0 && depth > 0 {
1382        i -= 1;
1383        if bytes[i] == close {
1384            depth += 1;
1385        } else if bytes[i] == open {
1386            depth -= 1;
1387        }
1388    }
1389    i
1390}
1391
1392/// Parse the expression chain before the dot into segments.
1393/// e.g. `poolManager.swap(key, params).` → [("poolManager", Plain), ("swap", Call)]
1394///      `_pools[poolId].fee.` → [("_pools", Index), ("fee", Plain)]
1395///      `msg.` → [("msg", Plain)]
1396/// True when the cursor is in a member-access completion.
1397///
1398/// Matches both `obj.|` and `obj.sta|`. The identifier after the dot is a
1399/// filter prefix, not a resolved member. Clients re-request completion after
1400/// the first `.` without sending the trigger character again.
1401pub fn is_member_completion(line: &str, character: u32) -> bool {
1402    let bytes = line.as_bytes();
1403    let mut pos = (character as usize).min(bytes.len());
1404    while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1405        pos -= 1;
1406    }
1407    pos > 0 && bytes[pos - 1] == b'.'
1408}
1409
1410pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1411    let col = character as usize;
1412    if col == 0 {
1413        return vec![];
1414    }
1415
1416    let bytes = line.as_bytes();
1417    let mut segments: Vec<DotSegment> = Vec::new();
1418
1419    // Start from the cursor. `obj.sta|` is still a completion on `obj`, so drop
1420    // the partial member name before parsing the chain.
1421    let mut pos = col.min(bytes.len());
1422    if is_member_completion(line, character) {
1423        while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1424            pos -= 1;
1425        }
1426    }
1427    if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1428        pos -= 1;
1429    }
1430
1431    loop {
1432        if pos == 0 {
1433            break;
1434        }
1435
1436        // Determine access kind by what's immediately before: ')' = Call, ']' = Index, else Plain
1437        let (kind, call_args) = if bytes[pos - 1] == b')' {
1438            let close = pos - 1; // position of ')'
1439            pos = skip_brackets_backwards(bytes, close);
1440            // Extract the text between '(' and ')'
1441            let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1442                .trim()
1443                .to_string();
1444            let args = if args_text.is_empty() {
1445                None
1446            } else {
1447                Some(args_text)
1448            };
1449            (AccessKind::Call, args)
1450        } else if bytes[pos - 1] == b']' {
1451            pos -= 1; // point to ']'
1452            pos = skip_brackets_backwards(bytes, pos);
1453            (AccessKind::Index, None)
1454        } else {
1455            (AccessKind::Plain, None)
1456        };
1457
1458        // Now extract the identifier name (walk backwards over alphanumeric + underscore)
1459        let end = pos;
1460        while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1461            pos -= 1;
1462        }
1463
1464        if pos == end {
1465            // No identifier found (could be something like `().`)
1466            break;
1467        }
1468
1469        let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1470        segments.push(DotSegment {
1471            name,
1472            kind,
1473            call_args,
1474        });
1475
1476        // Check if there's a dot before this segment (meaning more chain)
1477        if pos > 0 && bytes[pos - 1] == b'.' {
1478            pos -= 1; // skip the dot, continue parsing next segment
1479        } else {
1480            break;
1481        }
1482    }
1483
1484    segments.reverse(); // We parsed right-to-left, flip to left-to-right
1485    segments
1486}
1487
1488/// Extract the identifier before the cursor (the word before the dot).
1489/// Returns just the last identifier name for backward compatibility.
1490pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1491    let segments = parse_dot_chain(line, character);
1492    segments.last().map(|s| s.name.clone())
1493}
1494
1495#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1496Solidity AST uses different suffixes in different contexts:
1497  - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1498  - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1499  - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1500All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1501fn strip_type_suffix(type_id: &str) -> &str {
1502    let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1503    s.strip_suffix("_storage")
1504        .or_else(|| s.strip_suffix("_memory"))
1505        .or_else(|| s.strip_suffix("_calldata"))
1506        .unwrap_or(s)
1507}
1508
1509/// Look up using-for completions for a type, trying suffix variants.
1510/// The AST stores types with different suffixes (_storage_ptr, _storage, _memory_ptr, etc.)
1511/// across different contexts, so we try multiple forms.
1512fn lookup_using_for(
1513    cache: &CompletionCache,
1514    type_id: &str,
1515    node_id: Option<NodeId>,
1516) -> Vec<CompletionItem> {
1517    // Exact match first
1518    if let Some(items) = cache.using_for.get(type_id) {
1519        return items.clone();
1520    }
1521
1522    // A contract name that has no entry in `type_to_node` resolves to the
1523    // synthetic `__node_id_N` marker, which can never match a real key like
1524    // `t_contract$_IHooks_$1840`. Recover by matching on the node id instead.
1525    if let Some(nid) = node_id
1526        && type_id.starts_with("__node_id_")
1527        && let Some((_, items)) = cache
1528            .using_for
1529            .iter()
1530            .find(|(tid, _)| extract_node_id_from_type(tid.as_str()) == Some(nid))
1531    {
1532        return items.clone();
1533    }
1534
1535    // Strip to base form, then try all common suffix variants
1536    let base = strip_type_suffix(type_id);
1537    let variants = [
1538        base.to_string(),
1539        format!("{}_storage", base),
1540        format!("{}_storage_ptr", base),
1541        format!("{}_memory", base),
1542        format!("{}_memory_ptr", base),
1543        format!("{}_calldata", base),
1544    ];
1545    for variant in &variants {
1546        if variant.as_str() != type_id
1547            && let Some(items) = cache.using_for.get(variant.as_str())
1548        {
1549            return items.clone();
1550        }
1551    }
1552
1553    vec![]
1554}
1555
1556/// Split an array typeIdentifier into its element type and length.
1557///
1558/// `t_array$_t_uint256_$dyn_memory_ptr` → (`t_uint256`, `dyn`);
1559/// `t_array$_t_struct$_Log_$12_memory_ptr_$3_storage` → (`t_struct$_Log_$12_memory_ptr`, `3`).
1560/// The outermost length is the last `_$` segment once the location suffix is gone.
1561fn split_array_type(type_id: &str) -> Option<(&str, &str)> {
1562    let inner = strip_type_suffix(type_id).strip_prefix("t_array$_")?;
1563    inner.rsplit_once("_$")
1564}
1565
1566/// The type produced by indexing a value of `type_id`: a mapping's value type,
1567/// an array's element type, or `bytes1` for `bytes`.
1568fn index_result_type(type_id: &str) -> Option<String> {
1569    if type_id.starts_with("t_mapping") {
1570        return extract_mapping_value_type(type_id);
1571    }
1572    if let Some((element, _)) = split_array_type(type_id) {
1573        return Some(element.to_string());
1574    }
1575    if strip_type_suffix(type_id) == "t_bytes" {
1576        return Some("t_bytes1".to_string());
1577    }
1578    None
1579}
1580
1581/// Built-in members of arrays and byte types: `length` everywhere, plus
1582/// `push` and `pop` on dynamic arrays and `bytes` in storage. `string` has none.
1583fn builtin_type_members(type_id: &str) -> Vec<CompletionItem> {
1584    let is_dynamic_bytes = strip_type_suffix(type_id) == "t_bytes";
1585    let is_dynamic_array = split_array_type(type_id).is_some_and(|(_, len)| len == "dyn");
1586    let is_fixed = split_array_type(type_id).is_some_and(|(_, len)| len != "dyn")
1587        || type_id
1588            .strip_prefix("t_bytes")
1589            .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
1590
1591    let mut items = Vec::new();
1592    if is_dynamic_bytes || is_dynamic_array || is_fixed {
1593        items.push(builtin_member(
1594            "length",
1595            CompletionItemKind::PROPERTY,
1596            Some("uint256"),
1597        ));
1598    }
1599    if (is_dynamic_bytes || is_dynamic_array) && type_id.contains("_storage") {
1600        items.push(builtin_member(
1601            "push(...)",
1602            CompletionItemKind::METHOD,
1603            None,
1604        ));
1605        items.push(builtin_member("pop()", CompletionItemKind::METHOD, None));
1606    }
1607    items
1608}
1609
1610/// Synthetic type for a function, error, or event reached through a member
1611/// access (`X.fn.`): the only member that follows is `selector`.
1612const SELECTOR_MEMBER_TYPE: &str = "__selector_member";
1613
1614/// Collect completions available for a given typeIdentifier.
1615/// Includes node_members, method_identifiers, using_for, and using_for_wildcard.
1616fn completions_for_type(
1617    cache: &CompletionCache,
1618    type_id: &str,
1619    receiver: ReceiverKind,
1620) -> Vec<CompletionItem> {
1621    if type_id == SELECTOR_MEMBER_TYPE {
1622        return vec![builtin_member(
1623            "selector",
1624            CompletionItemKind::PROPERTY,
1625            Some("bytes4"),
1626        )];
1627    }
1628
1629    // Address type
1630    if type_id == "t_address" || type_id == "t_address_payable" {
1631        let mut items = address_members();
1632        // Also add using-for on address
1633        if let Some(uf) = cache.using_for.get(type_id) {
1634            items.extend(uf.iter().cloned());
1635        }
1636        items.extend(cache.using_for_wildcard.iter().cloned());
1637        return items;
1638    }
1639
1640    let resolved_node_id = extract_node_id_from_type(type_id)
1641        .or_else(|| cache.type_to_node.get(type_id).copied())
1642        .or_else(|| {
1643            // Handle synthetic __node_id_ markers from name_to_node_id fallback
1644            type_id
1645                .strip_prefix("__node_id_")
1646                .and_then(|s| s.parse::<i64>().ok())
1647                .map(NodeId)
1648        });
1649
1650    let mut items = builtin_type_members(type_id);
1651    let mut seen_labels: std::collections::HashSet<String> =
1652        items.iter().map(|item| item.label.clone()).collect();
1653
1654    if let Some(node_id) = resolved_node_id {
1655        // Method identifiers first — they have full signatures with selectors
1656        if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1657            for item in method_items {
1658                seen_labels.insert(item.label.clone());
1659                items.push(item.clone());
1660            }
1661        }
1662
1663        // Supplement with node_members (state variables, events, errors, modifiers, etc.)
1664        if let Some(members) = cache.node_members.get(&node_id) {
1665            for item in members {
1666                if !seen_labels.contains(&item.label) {
1667                    seen_labels.insert(item.label.clone());
1668                    items.push(item.clone());
1669                }
1670            }
1671        }
1672    }
1673
1674    // Add using-for library functions, but only when the receiver is a value —
1675    // not when it names a type. Typing `Lock.` should offer Lock's own members,
1676    // not functions from `using Pool for *` or `using SafeCast for *`.
1677    //
1678    // This has to come from the caller. Deriving it from the type (`is this a
1679    // contract?`) conflated `IERC20.` with a `paymentToken` declared as
1680    // `IERC20`, because both carry the same typeIdentifier — which meant
1681    // `using SafeERC20 for IERC20` extensions never showed up on the value.
1682    if receiver == ReceiverKind::Value {
1683        // Try exact match first, then try normalized variants (storage_ptr vs storage vs memory_ptr etc.)
1684        let uf_items = lookup_using_for(cache, type_id, resolved_node_id);
1685        for item in &uf_items {
1686            if !seen_labels.contains(&item.label) {
1687                seen_labels.insert(item.label.clone());
1688                items.push(item.clone());
1689            }
1690        }
1691
1692        // Add wildcard using-for (using X for *)
1693        for item in &cache.using_for_wildcard {
1694            if !seen_labels.contains(&item.label) {
1695                seen_labels.insert(item.label.clone());
1696                items.push(item.clone());
1697            }
1698        }
1699    }
1700
1701    items
1702}
1703
1704/// Whether the expression before the dot *names a type* or *is a value* of one.
1705///
1706/// Both produce the same `typeIdentifier` — `IERC20.` and a `paymentToken`
1707/// declared as `IERC20` both resolve to `t_contract$_IERC20_$N` — so the
1708/// distinction cannot be recovered from the type alone and has to be carried
1709/// down from wherever the receiver was resolved.
1710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1711enum ReceiverKind {
1712    /// `IERC20.` — the contract/library/interface itself.
1713    TypeName,
1714    /// `paymentToken.` — a value whose type is that contract.
1715    Value,
1716}
1717
1718/// Resolve a type identifier for a name, considering name_to_type and
1719/// name_to_node_id, reporting whether the name was a variable (a value) or a
1720/// contract/library/interface name (a type).
1721fn resolve_name_to_type_id_kind(
1722    cache: &CompletionCache,
1723    name: &str,
1724) -> Option<(String, ReceiverKind)> {
1725    // Direct type lookup — the name is a variable, so this is a value.
1726    if let Some(tid) = cache.name_to_type.get(name) {
1727        return Some((tid.to_string(), ReceiverKind::Value));
1728    }
1729    // Contract/library/interface name → synthesize a type id from node id
1730    if let Some(node_id) = cache.name_to_node_id.get(name) {
1731        // Find a matching typeIdentifier in type_to_node (reverse lookup)
1732        for (tid, nid) in &cache.type_to_node {
1733            if nid == node_id {
1734                return Some((tid.to_string(), ReceiverKind::TypeName));
1735            }
1736        }
1737        // Fallback: use a synthetic marker so completions_for_type can resolve via node_id
1738        return Some((format!("__node_id_{}", node_id), ReceiverKind::TypeName));
1739    }
1740    None
1741}
1742
1743/// Map a byte offset in the live buffer to the same place in `compiled`, the
1744/// text the AST (and so `scope_ranges`) was built from.
1745///
1746/// Incomplete code like `obj.` rarely compiles, so the cache usually lags the
1747/// buffer. Without this, every byte typed above the cursor since the last good
1748/// build shifts the scope lookup, and enough of them land it in a sibling
1749/// function. The two texts are compared by common prefix and suffix: offsets
1750/// before the edited region are unchanged, offsets after it shift by the
1751/// length difference. Offsets inside it are mapped from the suffix end, since
1752/// the cursor is normally where the most recent typing is.
1753pub fn map_live_offset_to_compiled(live: &str, compiled: &str, pos: usize) -> usize {
1754    let (l, c) = (live.as_bytes(), compiled.as_bytes());
1755    let prefix = l.iter().zip(c).take_while(|(a, b)| a == b).count();
1756    if pos <= prefix {
1757        return pos;
1758    }
1759    let max_suffix = l.len().min(c.len()) - prefix;
1760    let suffix = l
1761        .iter()
1762        .rev()
1763        .zip(c.iter().rev())
1764        .take(max_suffix)
1765        .take_while(|(a, b)| a == b)
1766        .count();
1767    let shifted = (pos + c.len()).saturating_sub(l.len());
1768    if pos >= l.len() - suffix {
1769        return shifted;
1770    }
1771    shifted.clamp(prefix, c.len() - suffix)
1772}
1773
1774/// Add the parameters and locals visible at the cursor.
1775///
1776/// These names are omitted from the flat general list, so a parameter like
1777/// `startAt` on another function or event does not show up everywhere.
1778/// Struct fields and event parameters stay out: their scopes are not on this chain.
1779/// Without a resolvable scope, every parameter and local is offered instead.
1780fn append_scoped_variables(
1781    cache: &CompletionCache,
1782    items: &mut Vec<CompletionItem>,
1783    ctx: Option<&ScopeContext>,
1784) {
1785    let mut seen: HashSet<&str> = HashSet::new();
1786    let mut scoped = Vec::new();
1787    let in_scope = ctx.is_some_and(|ctx| {
1788        walk_visible_declarations(cache, ctx.byte_pos, ctx.file_id, |decl| {
1789            if !cache.general_labels.contains(&decl.name) && seen.insert(&decl.name) {
1790                scoped.push(CompletionItem {
1791                    label: decl.name.clone(),
1792                    kind: Some(CompletionItemKind::VARIABLE),
1793                    detail: decl.type_string.clone(),
1794                    ..Default::default()
1795                });
1796            }
1797            ControlFlow::Continue(())
1798        })
1799    });
1800    if in_scope {
1801        items.extend(scoped);
1802    } else {
1803        items.extend(cache.unscoped_variables.iter().cloned());
1804    }
1805}
1806
1807/// Visit every declaration visible at `byte_pos`, innermost scope first: each
1808/// scope's own declarations, then (for a contract) the non-private ones of its
1809/// bases in C3 order, then the enclosing scope. Locals declared after
1810/// `byte_pos` are skipped, since a Solidity local is visible only after its
1811/// declaration. `visit` can stop the walk early with `ControlFlow::Break`.
1812///
1813/// Returns false when `byte_pos` is in no known scope.
1814fn walk_visible_declarations<'a>(
1815    cache: &'a CompletionCache,
1816    byte_pos: usize,
1817    file_id: FileId,
1818    mut visit: impl FnMut(&'a ScopedDeclaration) -> ControlFlow<()>,
1819) -> bool {
1820    let Some(mut current) = find_innermost_scope(cache, byte_pos, file_id) else {
1821        return false;
1822    };
1823    let declared_before =
1824        |decl: &ScopedDeclaration| decl.declared_at.is_none_or(|at| at < byte_pos);
1825    loop {
1826        for decl in cache.scope_declarations.get(&current).into_iter().flatten() {
1827            if declared_before(decl) && visit(decl).is_break() {
1828                return true;
1829            }
1830        }
1831        for &base in cache
1832            .linearized_base_contracts
1833            .get(&current)
1834            .into_iter()
1835            .flatten()
1836            .skip(1)
1837        {
1838            for decl in cache.scope_declarations.get(&base).into_iter().flatten() {
1839                if !decl.is_private && visit(decl).is_break() {
1840                    return true;
1841                }
1842            }
1843        }
1844        match cache.scope_parent.get(&current) {
1845            Some(&parent) => current = parent,
1846            None => return true,
1847        }
1848    }
1849}
1850
1851/// Find the innermost scope node that contains the given byte position and file.
1852/// `scope_ranges` must be sorted by span size ascending (smallest first).
1853/// Returns the node_id of the smallest scope enclosing the position.
1854pub fn find_innermost_scope(
1855    cache: &CompletionCache,
1856    byte_pos: usize,
1857    file_id: FileId,
1858) -> Option<NodeId> {
1859    // scope_ranges is sorted smallest-first, so the first match is the innermost scope
1860    cache
1861        .scope_ranges
1862        .iter()
1863        .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1864        .map(|r| r.node_id)
1865}
1866
1867/// Resolve a variable name to its type by walking up the scope chain.
1868///
1869/// Starting from the innermost scope at the cursor position, check each scope's
1870/// declarations for a matching name. If not found, follow `scope_parent` to the
1871/// next enclosing scope and check again. Stop at the first match.
1872///
1873/// Falls back to `resolve_name_to_type_id_kind` (flat lookup) if scope resolution
1874/// finds nothing, or if the scope data is unavailable.
1875pub fn resolve_name_in_scope(
1876    cache: &CompletionCache,
1877    name: &str,
1878    byte_pos: usize,
1879    file_id: FileId,
1880) -> Option<String> {
1881    resolve_name_in_scope_kind(cache, name, byte_pos, file_id).map(|(tid, _)| tid)
1882}
1883
1884/// Like [`resolve_name_in_scope`], but also reports whether the resolved name
1885/// was a declaration in scope (a value) or a contract name (a type).
1886fn resolve_name_in_scope_kind(
1887    cache: &CompletionCache,
1888    name: &str,
1889    byte_pos: usize,
1890    file_id: FileId,
1891) -> Option<(String, ReceiverKind)> {
1892    let mut found = None;
1893    walk_visible_declarations(cache, byte_pos, file_id, |decl| {
1894        if decl.name == name {
1895            found = Some((decl.type_id.clone(), ReceiverKind::Value));
1896            return ControlFlow::Break(());
1897        }
1898        ControlFlow::Continue(())
1899    });
1900
1901    // Not declared in scope (or the cursor is in no known scope): fall back to
1902    // the flat lookup, which also covers contract/library names.
1903    found.or_else(|| resolve_name_to_type_id_kind(cache, name))
1904}
1905
1906/// Resolve a name within a type context to get the member's type.
1907/// `context_type_id` is the type of the object before the dot.
1908/// `member_name` is the name after the dot.
1909/// `kind` determines how to interpret the result (Call = return type, Index = mapping value, Plain = member type).
1910fn resolve_member_type(
1911    cache: &CompletionCache,
1912    context_type_id: &str,
1913    member_name: &str,
1914    kind: &AccessKind,
1915) -> Option<String> {
1916    let resolved_node_id = extract_node_id_from_type(context_type_id)
1917        .or_else(|| cache.type_to_node.get(context_type_id).copied())
1918        .or_else(|| {
1919            // Handle synthetic __node_id_ markers
1920            context_type_id
1921                .strip_prefix("__node_id_")
1922                .and_then(|s| s.parse::<i64>().ok())
1923                .map(NodeId)
1924        });
1925
1926    let node_id = resolved_node_id?;
1927
1928    match kind {
1929        AccessKind::Call => {
1930            // Look up the function's return type
1931            cache
1932                .function_return_types
1933                .get(&(node_id, member_name.to_string()))
1934                .cloned()
1935        }
1936        AccessKind::Index => {
1937            // Look up the member's type, then extract mapping value type
1938            if let Some(members) = cache.node_members.get(&node_id) {
1939                for member in members {
1940                    if member.label == member_name {
1941                        // Get the typeIdentifier from name_to_type
1942                        if let Some(tid) = cache.name_to_type.get(member_name) {
1943                            return index_result_type(tid.as_str()).or(Some(tid.to_string()));
1944                        }
1945                    }
1946                }
1947            }
1948            // Also check: the identifier itself might be a mapping variable
1949            cache
1950                .name_to_type
1951                .get(member_name)
1952                .and_then(|tid| index_result_type(tid.as_str()))
1953        }
1954        AccessKind::Plain => {
1955            // `X.fn.`, `X.SomeError.`, `X.SomeEvent.` — only `.selector` follows.
1956            let is_callable_member = cache.node_members.get(&node_id).is_some_and(|members| {
1957                members.iter().any(|m| {
1958                    m.label == member_name
1959                        && matches!(
1960                            m.kind,
1961                            Some(CompletionItemKind::FUNCTION | CompletionItemKind::EVENT)
1962                        )
1963                })
1964            }) || cache
1965                .method_identifiers
1966                .get(&node_id)
1967                .is_some_and(|methods| methods.iter().any(|m| m.label == member_name));
1968            if is_callable_member {
1969                return Some(SELECTOR_MEMBER_TYPE.to_string());
1970            }
1971            // `Lib.Enum.` — a nested enum names a type; resolve it to its declaration.
1972            if let Some(enum_id) = cache
1973                .nested_types
1974                .get(&(node_id, SymbolName::new(member_name)))
1975            {
1976                return Some(format!("__node_id_{}", enum_id));
1977            }
1978            // Look up member's own type from name_to_type
1979            cache
1980                .name_to_type
1981                .get(member_name)
1982                .map(|tid| tid.to_string())
1983        }
1984    }
1985}
1986
1987/// Scope context for scope-aware completion resolution.
1988/// When present, type resolution uses the scope chain at the cursor position
1989/// instead of the flat first-wins `name_to_type` map.
1990pub struct ScopeContext {
1991    /// Byte offset of the cursor in the source file.
1992    pub byte_pos: usize,
1993    /// Source file id (from the AST `src` field).
1994    pub file_id: FileId,
1995}
1996
1997/// Resolve a name to a type, using scope context if available.
1998/// With scope context: walks up the scope chain from the cursor position.
1999/// Without: falls back to flat `name_to_type` lookup.
2000fn resolve_name(
2001    cache: &CompletionCache,
2002    name: &str,
2003    scope_ctx: Option<&ScopeContext>,
2004) -> Option<String> {
2005    resolve_name_kind(cache, name, scope_ctx).map(|(tid, _)| tid)
2006}
2007
2008/// Like [`resolve_name`], but also reports whether the name was a value or a type.
2009fn resolve_name_kind(
2010    cache: &CompletionCache,
2011    name: &str,
2012    scope_ctx: Option<&ScopeContext>,
2013) -> Option<(String, ReceiverKind)> {
2014    if let Some(ctx) = scope_ctx {
2015        resolve_name_in_scope_kind(cache, name, ctx.byte_pos, ctx.file_id)
2016    } else {
2017        resolve_name_to_type_id_kind(cache, name)
2018    }
2019}
2020
2021/// Get completions for a dot-completion request by resolving the full expression chain.
2022pub fn get_dot_completions(
2023    cache: &CompletionCache,
2024    identifier: &str,
2025    scope_ctx: Option<&ScopeContext>,
2026) -> Vec<CompletionItem> {
2027    // Simple single-segment case (backward compat) — just use the identifier directly
2028    if let Some(items) = magic_members(identifier) {
2029        return items;
2030    }
2031    if let Some(items) = cache.type_name_members.get(identifier) {
2032        return items.clone();
2033    }
2034
2035    // Plain access, so a contract name stays a type name and a variable stays
2036    // a value — exactly the distinction using-for extensions hinge on.
2037    if let Some((tid, receiver)) = resolve_name_kind(cache, identifier, scope_ctx) {
2038        return completions_for_type(cache, &tid, receiver);
2039    }
2040
2041    vec![]
2042}
2043
2044/// Get completions by resolving a full dot-expression chain.
2045/// This is the main entry point for dot-completions with chaining support.
2046pub fn get_chain_completions(
2047    cache: &CompletionCache,
2048    chain: &[DotSegment],
2049    scope_ctx: Option<&ScopeContext>,
2050) -> Vec<CompletionItem> {
2051    if chain.is_empty() {
2052        return vec![];
2053    }
2054
2055    // Single segment: simple dot-completion
2056    if chain.len() == 1 {
2057        let seg = &chain[0];
2058
2059        // For Call/Index on the single segment, we need to resolve the return/value type
2060        match seg.kind {
2061            AccessKind::Plain => {
2062                return get_dot_completions(cache, &seg.name, scope_ctx);
2063            }
2064            AccessKind::Call => {
2065                // type(X). — Solidity metatype expression
2066                if seg.name == "type" {
2067                    return type_meta_members(seg.call_args.as_deref(), Some(cache));
2068                }
2069                // foo(). — could be a function call or a type cast like IFoo(addr).
2070                // First check if it's a type cast: name matches a contract/interface/library
2071                // A cast yields a *value* of that type, so extensions apply even
2072                // though the name itself is a contract.
2073                if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
2074                    return completions_for_type(cache, &type_id, ReceiverKind::Value);
2075                }
2076                // Otherwise look up as a function call — check all function_return_types
2077                for ((_, fn_name), ret_type) in &cache.function_return_types {
2078                    if fn_name == &seg.name {
2079                        return completions_for_type(cache, ret_type, ReceiverKind::Value);
2080                    }
2081                }
2082                return vec![];
2083            }
2084            AccessKind::Index => {
2085                // foo[key]. — a mapping's value type or an array's element type
2086                if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
2087                    && let Some(val_type) = index_result_type(&tid)
2088                {
2089                    return completions_for_type(cache, &val_type, ReceiverKind::Value);
2090                }
2091                return vec![];
2092            }
2093        }
2094    }
2095
2096    // Multi-segment chain: resolve step by step
2097    // First segment: resolve to a type (scope-aware when available)
2098    let first = &chain[0];
2099    let mut current_type = match first.kind {
2100        AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
2101        AccessKind::Call => {
2102            // Type cast (e.g. IFoo(addr).) or free function call at the start
2103            resolve_name(cache, &first.name, scope_ctx).or_else(|| {
2104                cache
2105                    .function_return_types
2106                    .iter()
2107                    .find(|((_, fn_name), _)| fn_name == &first.name)
2108                    .map(|(_, ret_type)| ret_type.clone())
2109            })
2110        }
2111        AccessKind::Index => {
2112            // Mapping or array access at the start
2113            resolve_name(cache, &first.name, scope_ctx)
2114                .and_then(|tid| index_result_type(&tid).or(Some(tid)))
2115        }
2116    };
2117
2118    // Middle segments: resolve each to advance the type
2119    for seg in &chain[1..] {
2120        let ctx_type = match &current_type {
2121            Some(t) => t.clone(),
2122            None => return vec![],
2123        };
2124
2125        current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
2126    }
2127
2128    // Return completions for the final resolved type. Anything reached by
2129    // walking members is a value, except a nested type (`Lib.Enum.`), which
2130    // resolves to a synthetic `__node_id_` marker and names a type.
2131    match current_type {
2132        Some(tid) => {
2133            let receiver = if tid.starts_with("__node_id_") {
2134                ReceiverKind::TypeName
2135            } else {
2136                ReceiverKind::Value
2137            };
2138            completions_for_type(cache, &tid, receiver)
2139        }
2140        None => vec![],
2141    }
2142}
2143
2144/// Get static completions that never change (keywords, magic globals, global functions, units).
2145/// These are available immediately without an AST cache.
2146pub fn get_static_completions() -> Vec<CompletionItem> {
2147    let mut items = Vec::new();
2148
2149    // Add Solidity keywords
2150    for kw in SOLIDITY_KEYWORDS {
2151        items.push(CompletionItem {
2152            label: kw.to_string(),
2153            kind: Some(CompletionItemKind::KEYWORD),
2154            ..Default::default()
2155        });
2156    }
2157
2158    // Add magic globals
2159    for (name, detail) in MAGIC_GLOBALS {
2160        items.push(CompletionItem {
2161            label: name.to_string(),
2162            kind: Some(CompletionItemKind::VARIABLE),
2163            detail: Some(detail.to_string()),
2164            ..Default::default()
2165        });
2166    }
2167
2168    // Add global functions
2169    for (name, detail) in GLOBAL_FUNCTIONS {
2170        items.push(CompletionItem {
2171            label: name.to_string(),
2172            kind: Some(CompletionItemKind::FUNCTION),
2173            detail: Some(detail.to_string()),
2174            ..Default::default()
2175        });
2176    }
2177
2178    // Add ether denomination units
2179    for (name, detail) in ETHER_UNITS {
2180        items.push(CompletionItem {
2181            label: name.to_string(),
2182            kind: Some(CompletionItemKind::UNIT),
2183            detail: Some(detail.to_string()),
2184            ..Default::default()
2185        });
2186    }
2187
2188    // Add time units
2189    for (name, detail) in TIME_UNITS {
2190        items.push(CompletionItem {
2191            label: name.to_string(),
2192            kind: Some(CompletionItemKind::UNIT),
2193            detail: Some(detail.to_string()),
2194            ..Default::default()
2195        });
2196    }
2197
2198    items
2199}
2200
2201/// Get general completions (all known names + static completions).
2202pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
2203    let mut items = cache.names.clone();
2204    items.extend(get_static_completions());
2205    items
2206}
2207
2208fn completion_item(
2209    label: impl Into<String>,
2210    kind: CompletionItemKind,
2211    detail: impl Into<String>,
2212) -> CompletionItem {
2213    CompletionItem {
2214        label: label.into(),
2215        kind: Some(kind),
2216        detail: Some(detail.into()),
2217        ..Default::default()
2218    }
2219}
2220
2221fn line_replacement_item(
2222    label: impl Into<String>,
2223    detail: impl Into<String>,
2224    new_text: impl Into<String>,
2225    position: Position,
2226) -> CompletionItem {
2227    let label = label.into();
2228    let new_text = new_text.into();
2229    CompletionItem {
2230        filter_text: Some(format!("spdx {label} {new_text}")),
2231        label,
2232        kind: Some(CompletionItemKind::CONSTANT),
2233        detail: Some(detail.into()),
2234        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
2235            range: Range {
2236                start: Position {
2237                    line: position.line,
2238                    character: 0,
2239                },
2240                end: position,
2241            },
2242            new_text,
2243        })),
2244        ..Default::default()
2245    }
2246}
2247
2248fn prefix_at_byte(line: &str, col_byte: u32) -> &str {
2249    let mut end = (col_byte as usize).min(line.len());
2250    while !line.is_char_boundary(end) {
2251        end -= 1;
2252    }
2253    &line[..end]
2254}
2255
2256fn source_unit_directive_completions(
2257    line: &str,
2258    line_prefix: &str,
2259    position: Position,
2260) -> Option<Vec<CompletionItem>> {
2261    let trimmed = line_prefix.trim_start();
2262
2263    if let Some(after_slashes) = trimmed.strip_prefix("//") {
2264        let body = after_slashes.trim_start();
2265        let upper = body.to_ascii_uppercase();
2266
2267        if upper.starts_with("SPDX-LICENSE-IDENTIFIER:") {
2268            return Some(spdx_license_identifier_completions());
2269        }
2270
2271        if is_spdx_directive_prefix(&upper) {
2272            return Some(spdx_directive_completions(position));
2273        }
2274    }
2275
2276    if let Some(after_solidity) = strip_directive_value(trimmed, "pragma solidity") {
2277        if after_solidity.contains(';') {
2278            return Some(vec![]);
2279        }
2280
2281        return Some(solidity_version_completions(
2282            position,
2283            directive_value_range(line, "pragma solidity"),
2284        ));
2285    }
2286
2287    if let Some(after_abicoder) = strip_directive_value(trimmed, "pragma abicoder") {
2288        if after_abicoder.contains(';') {
2289            return Some(vec![]);
2290        }
2291
2292        return Some(vec![
2293            completion_item("v2", CompletionItemKind::CONSTANT, "ABI coder v2"),
2294            completion_item("v1", CompletionItemKind::CONSTANT, "ABI coder v1"),
2295        ]);
2296    }
2297
2298    if let Some(after_pragma) = trimmed.strip_prefix("pragma")
2299        && !after_pragma.is_empty()
2300        && after_pragma.trim().is_empty()
2301    {
2302        return Some(vec![
2303            completion_item(
2304                "solidity",
2305                CompletionItemKind::KEYWORD,
2306                "Compiler version pragma",
2307            ),
2308            completion_item("abicoder", CompletionItemKind::KEYWORD, "ABI coder pragma"),
2309        ]);
2310    }
2311
2312    None
2313}
2314
2315fn strip_directive_value<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
2316    let rest = line.strip_prefix(directive)?;
2317    if rest.is_empty() || rest.starts_with(char::is_whitespace) {
2318        Some(rest)
2319    } else {
2320        None
2321    }
2322}
2323
2324fn is_spdx_directive_prefix(upper_comment_body: &str) -> bool {
2325    let body = upper_comment_body.trim_start();
2326    !body.is_empty() && ("SPDX".starts_with(body) || body.starts_with("SPDX"))
2327}
2328
2329fn spdx_directive_completions(position: Position) -> Vec<CompletionItem> {
2330    vec![
2331        line_replacement_item(
2332            "// SPDX-License-Identifier: MIT",
2333            "SPDX license identifier",
2334            "// SPDX-License-Identifier: MIT",
2335            position,
2336        ),
2337        line_replacement_item(
2338            "// SPDX-License-Identifier: UNLICENSED",
2339            "SPDX license identifier",
2340            "// SPDX-License-Identifier: UNLICENSED",
2341            position,
2342        ),
2343        line_replacement_item(
2344            "// SPDX-License-Identifier: Apache-2.0",
2345            "SPDX license identifier",
2346            "// SPDX-License-Identifier: Apache-2.0",
2347            position,
2348        ),
2349    ]
2350}
2351
2352fn spdx_license_identifier_completions() -> Vec<CompletionItem> {
2353    [
2354        "MIT",
2355        "UNLICENSED",
2356        "Apache-2.0",
2357        "GPL-3.0-only",
2358        "GPL-3.0-or-later",
2359        "BSD-3-Clause",
2360    ]
2361    .into_iter()
2362    .map(|license| completion_item(license, CompletionItemKind::CONSTANT, "SPDX license"))
2363    .collect()
2364}
2365
2366fn directive_value_range(line: &str, directive: &str) -> Range {
2367    let mut byte = line
2368        .find(directive)
2369        .map(|idx| idx + directive.len())
2370        .unwrap_or(line.len());
2371
2372    while byte < line.len() {
2373        let ch = line[byte..]
2374            .chars()
2375            .next()
2376            .expect("byte is within string bounds");
2377        if !ch.is_whitespace() {
2378            break;
2379        }
2380        byte += ch.len_utf8();
2381    }
2382
2383    let end_byte = line[byte..]
2384        .find(';')
2385        .map(|idx| byte + idx)
2386        .unwrap_or(line.len());
2387
2388    Range {
2389        start: Position {
2390            line: 0,
2391            character: line[..byte].encode_utf16().count() as u32,
2392        },
2393        end: Position {
2394            line: 0,
2395            character: line[..end_byte].encode_utf16().count() as u32,
2396        },
2397    }
2398}
2399
2400fn version_completion_item(
2401    label: impl Into<String>,
2402    detail: impl Into<String>,
2403    position: Position,
2404    range: Range,
2405) -> CompletionItem {
2406    let label = label.into();
2407    CompletionItem {
2408        label: label.clone(),
2409        kind: Some(CompletionItemKind::CONSTANT),
2410        detail: Some(detail.into()),
2411        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
2412            range: Range {
2413                start: Position {
2414                    line: position.line,
2415                    character: range.start.character,
2416                },
2417                end: Position {
2418                    line: position.line,
2419                    character: range.end.character,
2420                },
2421            },
2422            new_text: label,
2423        })),
2424        ..Default::default()
2425    }
2426}
2427
2428fn solidity_version_completions(position: Position, range: Range) -> Vec<CompletionItem> {
2429    [
2430        ("^0.8.35", "Latest Solidity 0.8.x compatible range"),
2431        ("0.8.35", "Latest Solidity exact version"),
2432        ("^0.8.0", "Solidity 0.8.x compatible range"),
2433        (">=0.8.0 <0.9.0", "Solidity 0.8.x range"),
2434    ]
2435    .into_iter()
2436    .map(|(version, detail)| version_completion_item(version, detail, position, range))
2437    .collect()
2438}
2439
2440/// Append auto-import candidates at the tail of completion results.
2441///
2442/// This enforces lower priority ordering by:
2443/// 1) appending after base completions
2444/// 2) assigning high `sortText` values (`zz_autoimport_*`) when missing
2445pub fn append_auto_import_candidates_last(
2446    mut base: Vec<CompletionItem>,
2447    mut auto_import_candidates: Vec<CompletionItem>,
2448) -> Vec<CompletionItem> {
2449    let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
2450    for item in &auto_import_candidates {
2451        let entry = unique_label_edits
2452            .entry(item.label.clone())
2453            .or_insert_with(|| item.additional_text_edits.clone());
2454        if *entry != item.additional_text_edits {
2455            *entry = None;
2456        }
2457    }
2458
2459    // If a label maps to exactly one import edit, attach it to the corresponding
2460    // base completion item too. This ensures accepting the normal item can still
2461    // apply import edits in clients that de-prioritize or collapse duplicate labels.
2462    for item in &mut base {
2463        if item.additional_text_edits.is_none()
2464            && let Some(Some(edits)) = unique_label_edits.get(&item.label)
2465        {
2466            item.additional_text_edits = Some(edits.clone());
2467        }
2468    }
2469
2470    for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
2471        if item.sort_text.is_none() {
2472            item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
2473        }
2474    }
2475
2476    base.extend(auto_import_candidates);
2477    base
2478}
2479
2480/// Convert cached top-level importable symbols into completion items.
2481///
2482/// These are intended as low-priority tail candidates appended after normal
2483/// per-file completions.
2484pub fn top_level_importable_completion_candidates(
2485    cache: &CompletionCache,
2486    current_file_path: Option<&str>,
2487    source_text: &str,
2488) -> Vec<CompletionItem> {
2489    let mut out = Vec::new();
2490    for symbols in cache.top_level_importables_by_name.values() {
2491        for symbol in symbols {
2492            if let Some(cur) = current_file_path
2493                && cur == symbol.declaring_path
2494            {
2495                continue;
2496            }
2497
2498            let import_path = match current_file_path.and_then(|cur| {
2499                to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
2500            }) {
2501                Some(p) => p,
2502                None => continue,
2503            };
2504
2505            if import_statement_already_present(source_text, &symbol.name, &import_path) {
2506                continue;
2507            }
2508
2509            let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
2510            out.push(CompletionItem {
2511                label: symbol.name.clone(),
2512                kind: Some(symbol.kind),
2513                detail: Some(format!("{} ({import_path})", symbol.node_type)),
2514                additional_text_edits: import_edit.map(|e| vec![e]),
2515                ..Default::default()
2516            });
2517        }
2518    }
2519    out
2520}
2521
2522fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
2523    let from_dir = current_file.parent()?;
2524    let rel = pathdiff::diff_paths(target_file, from_dir)?;
2525    let mut s = rel.to_string_lossy().replace('\\', "/");
2526    if !s.starts_with("./") && !s.starts_with("../") {
2527        s = format!("./{s}");
2528    }
2529    Some(s)
2530}
2531
2532fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
2533    let named = format!("import {{{symbol}}} from \"{import_path}\";");
2534    let full = format!("import \"{import_path}\";");
2535    source_text.contains(&named) || source_text.contains(&full)
2536}
2537
2538fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
2539    let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
2540    let lines: Vec<&str> = source_text.lines().collect();
2541
2542    let last_import_line = lines
2543        .iter()
2544        .enumerate()
2545        .filter(|(_, line)| line.trim_start().starts_with("import "))
2546        .map(|(idx, _)| idx)
2547        .next_back();
2548
2549    let insert_line = if let Some(idx) = last_import_line {
2550        idx + 1
2551    } else if let Some(idx) = lines
2552        .iter()
2553        .enumerate()
2554        .filter(|(_, line)| line.trim_start().starts_with("pragma "))
2555        .map(|(idx, _)| idx)
2556        .next_back()
2557    {
2558        idx + 1
2559    } else {
2560        0
2561    };
2562
2563    Some(TextEdit {
2564        range: Range {
2565            start: Position {
2566                line: insert_line as u32,
2567                character: 0,
2568            },
2569            end: Position {
2570                line: insert_line as u32,
2571                character: 0,
2572            },
2573        },
2574        new_text: import_stmt,
2575    })
2576}
2577
2578/// Handle a completion request with optional tail candidates.
2579///
2580/// Tail candidates are only appended for non-dot completions and are always
2581/// ordered last via `append_auto_import_candidates_last`.
2582pub fn handle_completion_with_tail_candidates(
2583    cache: Option<&CompletionCache>,
2584    source_text: &str,
2585    position: Position,
2586    trigger_char: Option<&str>,
2587    file_id: Option<FileId>,
2588    compiled_text: Option<&str>,
2589    tail_candidates: Vec<CompletionItem>,
2590) -> Option<CompletionResponse> {
2591    let lines: Vec<&str> = source_text.lines().collect();
2592    let line = lines.get(position.line as usize)?;
2593
2594    // Convert encoding-aware column to a byte offset within this line.
2595    let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
2596    let line_start_byte: usize = source_text[..abs_byte]
2597        .rfind('\n')
2598        .map(|i| i + 1)
2599        .unwrap_or(0);
2600    let col_byte = (abs_byte - line_start_byte) as u32;
2601
2602    if let Some(items) =
2603        source_unit_directive_completions(line, prefix_at_byte(line, col_byte), position)
2604    {
2605        return Some(CompletionResponse::List(CompletionList {
2606            is_incomplete: false,
2607            items,
2608        }));
2609    }
2610
2611    // Build scope context for scope-aware type resolution. Scope ranges are
2612    // offsets into the compiled text, so translate the live cursor into it.
2613    let scope_ctx = file_id.map(|fid| ScopeContext {
2614        byte_pos: compiled_text.map_or(abs_byte, |ct| {
2615            map_live_offset_to_compiled(source_text, ct, abs_byte)
2616        }),
2617        file_id: fid,
2618    });
2619
2620    let triggered_by_dot = trigger_char == Some(".");
2621    let member_items = if triggered_by_dot || is_member_completion(line, col_byte) {
2622        let chain = crate::completion::parse_dot_chain(line, col_byte);
2623        if chain.is_empty() && triggered_by_dot {
2624            return None;
2625        }
2626        Some(match cache {
2627            Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
2628            None => {
2629                // No cache yet — serve magic dot completions (msg., block., etc.)
2630                if chain.len() == 1 {
2631                    let seg = &chain[0];
2632                    if seg.name == "type" && seg.kind == AccessKind::Call {
2633                        // type(X). without cache — classify based on name alone
2634                        type_meta_members(seg.call_args.as_deref(), None)
2635                    } else if seg.kind == AccessKind::Plain {
2636                        magic_members(&seg.name).unwrap_or_default()
2637                    } else {
2638                        vec![]
2639                    }
2640                } else {
2641                    vec![]
2642                }
2643            }
2644        })
2645    } else {
2646        None
2647    };
2648
2649    let items = match member_items {
2650        // A member position nothing resolves on (`1.`, `/// see Foo.bar`, a local
2651        // the cache has not seen yet) still gets the general list when completion
2652        // was invoked rather than triggered by typing `.`.
2653        Some(items) if triggered_by_dot || !items.is_empty() => items,
2654        _ => match cache {
2655            Some(c) => {
2656                let mut items = c.general_completions.clone();
2657                append_scoped_variables(c, &mut items, scope_ctx.as_ref());
2658                append_auto_import_candidates_last(items, tail_candidates)
2659            }
2660            None => get_static_completions(),
2661        },
2662    };
2663
2664    Some(CompletionResponse::List(CompletionList {
2665        is_incomplete: cache.is_none(),
2666        items,
2667    }))
2668}
2669
2670/// Handle a completion request.
2671///
2672/// When `cache` is `Some`, full AST-aware completions are returned.
2673/// When `cache` is `None`, only static completions (keywords, globals, units)
2674/// and magic dot completions (msg., block., tx., abi., type().) are returned
2675/// immediately — no blocking.
2676///
2677/// `file_id` is the AST source file id, needed for scope-aware resolution.
2678/// When `None`, scope resolution is skipped and flat lookup is used.
2679pub fn handle_completion(
2680    cache: Option<&CompletionCache>,
2681    source_text: &str,
2682    position: Position,
2683    trigger_char: Option<&str>,
2684    file_id: Option<FileId>,
2685) -> Option<CompletionResponse> {
2686    handle_completion_with_tail_candidates(
2687        cache,
2688        source_text,
2689        position,
2690        trigger_char,
2691        file_id,
2692        None,
2693        vec![],
2694    )
2695}
2696
2697const SOLIDITY_KEYWORDS: &[&str] = &[
2698    "abstract",
2699    "address",
2700    "assembly",
2701    "bool",
2702    "break",
2703    "bytes",
2704    "bytes1",
2705    "bytes4",
2706    "bytes32",
2707    "calldata",
2708    "constant",
2709    "constructor",
2710    "continue",
2711    "contract",
2712    "delete",
2713    "do",
2714    "else",
2715    "emit",
2716    "enum",
2717    "error",
2718    "event",
2719    "external",
2720    "fallback",
2721    "false",
2722    "for",
2723    "function",
2724    "if",
2725    "immutable",
2726    "import",
2727    "indexed",
2728    "int8",
2729    "int24",
2730    "int128",
2731    "int256",
2732    "interface",
2733    "internal",
2734    "library",
2735    "mapping",
2736    "memory",
2737    "modifier",
2738    "new",
2739    "override",
2740    "payable",
2741    "pragma",
2742    "private",
2743    "public",
2744    "pure",
2745    "receive",
2746    "return",
2747    "returns",
2748    "revert",
2749    "storage",
2750    "string",
2751    "struct",
2752    "true",
2753    "type",
2754    "uint8",
2755    "uint24",
2756    "uint128",
2757    "uint160",
2758    "uint256",
2759    "unchecked",
2760    "using",
2761    "view",
2762    "virtual",
2763    "while",
2764];
2765
2766/// Ether denomination units — suffixes for literal numbers.
2767const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2768
2769/// Time units — suffixes for literal numbers.
2770const TIME_UNITS: &[(&str, &str)] = &[
2771    ("seconds", "1"),
2772    ("minutes", "60 seconds"),
2773    ("hours", "3600 seconds"),
2774    ("days", "86400 seconds"),
2775    ("weeks", "604800 seconds"),
2776];
2777
2778const MAGIC_GLOBALS: &[(&str, &str)] = &[
2779    ("msg", "msg"),
2780    ("block", "block"),
2781    ("tx", "tx"),
2782    ("abi", "abi"),
2783    ("this", "address"),
2784    ("super", "contract"),
2785    ("type", "type information"),
2786];
2787
2788const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2789    // Mathematical and Cryptographic Functions
2790    ("addmod(uint256, uint256, uint256)", "uint256"),
2791    ("mulmod(uint256, uint256, uint256)", "uint256"),
2792    ("keccak256(bytes memory)", "bytes32"),
2793    ("sha256(bytes memory)", "bytes32"),
2794    ("ripemd160(bytes memory)", "bytes20"),
2795    (
2796        "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2797        "address",
2798    ),
2799    // Block and Transaction Properties (functions)
2800    ("blockhash(uint256 blockNumber)", "bytes32"),
2801    ("blobhash(uint256 index)", "bytes32"),
2802    ("gasleft()", "uint256"),
2803    // Error Handling
2804    ("assert(bool condition)", ""),
2805    ("require(bool condition)", ""),
2806    ("require(bool condition, string memory message)", ""),
2807    ("revert()", ""),
2808    ("revert(string memory reason)", ""),
2809    // Contract-related
2810    ("selfdestruct(address payable recipient)", ""),
2811];
2812
2813// ---------------------------------------------------------------------------
2814// Import path completions
2815// ---------------------------------------------------------------------------
2816
2817/// Walk `project_root` recursively and return every `.sol` file as:
2818///   1. A relative path from the current file's directory (e.g. `./libraries/Pool.sol`)
2819///   2. A remapped path for each matching remapping (e.g. `forge-std/Test.sol`)
2820///
2821/// Skips `out/`, `cache/`, `node_modules/`, `.git/`.
2822///
2823/// `typed_range` is `Some((line, start_col, end_col))` where `start_col` is the
2824/// character column right after the opening quote and `end_col` is the cursor
2825/// column. When provided, each item gets a `text_edit` that replaces the
2826/// already-typed text so the insert doesn't double-up, plus `filter_text = label`
2827/// so clients filter on the full path rather than the word at cursor.
2828pub fn all_sol_import_paths(
2829    current_file: &Path,
2830    project_root: &Path,
2831    remappings: &[String],
2832    typed_range: Option<(u32, u32, u32)>,
2833) -> Vec<CompletionItem> {
2834    let current_dir = match current_file.parent() {
2835        Some(d) => d,
2836        None => return vec![],
2837    };
2838
2839    // Pre-parse remappings into (prefix, abs_target_path) pairs.
2840    // e.g. "forge-std/=lib/forge-std/src/" → ("forge-std/", "/project/lib/forge-std/src/")
2841    // Canonicalize the target so strip_prefix works even when the path contains
2842    // `..` segments (e.g. lib/forge-std/../src/ would otherwise produce bad labels).
2843    let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2844        .iter()
2845        .filter_map(|r| {
2846            let mut it = r.splitn(2, '=');
2847            let prefix = it.next()?.to_string();
2848            let target = it.next()?;
2849            if prefix.is_empty() || target.is_empty() {
2850                return None;
2851            }
2852            let raw = project_root.join(target);
2853            let canonical = raw.canonicalize().unwrap_or(raw);
2854            Some((prefix, canonical))
2855        })
2856        .collect();
2857
2858    let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2859    let mut items = Vec::new();
2860
2861    collect_sol_files(
2862        project_root,
2863        current_dir,
2864        &parsed_remappings,
2865        skip_dirs,
2866        typed_range,
2867        &mut items,
2868    );
2869    items.sort_by(|a, b| a.label.cmp(&b.label));
2870    items
2871}
2872
2873fn collect_sol_files(
2874    dir: &Path,
2875    current_dir: &Path,
2876    remappings: &[(String, std::path::PathBuf)],
2877    skip_dirs: &[&str],
2878    typed_range: Option<(u32, u32, u32)>,
2879    out: &mut Vec<CompletionItem>,
2880) {
2881    let entries = match std::fs::read_dir(dir) {
2882        Ok(e) => e,
2883        Err(_) => return,
2884    };
2885    for entry in entries.flatten() {
2886        let path = entry.path();
2887        let name = entry.file_name();
2888        let name_str = name.to_string_lossy();
2889
2890        if name_str.starts_with('.') {
2891            continue;
2892        }
2893
2894        if path.is_dir() {
2895            if skip_dirs.contains(&name_str.as_ref()) {
2896                continue;
2897            }
2898            collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2899            continue;
2900        }
2901
2902        if !path.is_file() || !name_str.ends_with(".sol") {
2903            continue;
2904        }
2905
2906        // 1. Relative path (always emitted)
2907        if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2908            let s = rel.to_string_lossy().to_string();
2909            let label = if s.starts_with("../") || s.starts_with("./") {
2910                s
2911            } else {
2912                format!("./{s}")
2913            };
2914            out.push(make_import_item(label, typed_range));
2915        }
2916
2917        // 2. Remapped paths — for every remapping whose target is a prefix of
2918        //    this file's absolute path, emit the remapped form.
2919        //    e.g. file=/project/lib/forge-std/src/Test.sol
2920        //         remap forge-std/=/project/lib/forge-std/src/
2921        //         → label = "forge-std/Test.sol"
2922        for (prefix, target_abs) in remappings {
2923            if let Ok(suffix) = path.strip_prefix(target_abs) {
2924                let suffix_str = suffix
2925                    .to_string_lossy()
2926                    .trim_start_matches(['/', '\\'])
2927                    .to_string();
2928                let label = format!("{}{}", prefix, suffix_str);
2929                out.push(make_import_item(label, typed_range));
2930            }
2931        }
2932    }
2933}
2934
2935fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2936    // filter_text = label so that clients whose word-at-cursor is shorter than
2937    // the full path (e.g. just "Pool" when the label is "./src/Pool.sol") still
2938    // match correctly via substring/fuzzy matching against the full path.
2939    //
2940    // text_edit replaces the already-typed text inside the quotes so the insert
2941    // doesn't append after what was already typed.
2942    let text_edit = typed_range.map(|(line, start_col, end_col)| {
2943        tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2944            range: Range {
2945                start: Position {
2946                    line,
2947                    character: start_col,
2948                },
2949                end: Position {
2950                    line,
2951                    character: end_col,
2952                },
2953            },
2954            new_text: label.clone(),
2955        })
2956    });
2957    CompletionItem {
2958        label: label.clone(),
2959        filter_text: Some(label.clone()),
2960        text_edit,
2961        kind: Some(CompletionItemKind::FILE),
2962        ..Default::default()
2963    }
2964}
2965
2966#[cfg(test)]
2967mod tests {
2968    use super::{
2969        CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2970        build_completion_cache, extract_top_level_importables_for_file, handle_completion,
2971    };
2972    use crate::types::{FileId, SymbolName};
2973    use serde_json::json;
2974    use std::collections::{HashMap, HashSet};
2975    use tower_lsp::lsp_types::CompletionItemKind;
2976    use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2977
2978    fn empty_cache() -> CompletionCache {
2979        CompletionCache {
2980            names: vec![],
2981            name_to_type: HashMap::new(),
2982            node_members: HashMap::new(),
2983            type_to_node: HashMap::new(),
2984            name_to_node_id: HashMap::new(),
2985            nested_types: HashMap::new(),
2986            method_identifiers: HashMap::new(),
2987            function_return_types: HashMap::new(),
2988            using_for: HashMap::new(),
2989            using_for_wildcard: vec![],
2990            general_completions: vec![],
2991            general_labels: HashSet::new(),
2992            unscoped_variables: vec![],
2993            scope_declarations: HashMap::new(),
2994            scope_parent: HashMap::new(),
2995            scope_ranges: vec![],
2996            path_to_file_id: HashMap::new(),
2997            linearized_base_contracts: HashMap::new(),
2998            contract_kinds: HashMap::new(),
2999            type_name_members: HashMap::new(),
3000            top_level_importables_by_name: HashMap::new(),
3001            top_level_importables_by_file: HashMap::new(),
3002        }
3003    }
3004
3005    #[test]
3006    fn top_level_importables_include_only_direct_declared_symbols() {
3007        let sources = json!({
3008            "/tmp/A.sol": {
3009                "id": 0,
3010                "ast": {
3011                    "id": 1,
3012                    "nodeType": "SourceUnit",
3013                    "src": "0:100:0",
3014                    "nodes": [
3015                        { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
3016                        { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
3017                            { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
3018                        ] },
3019                        { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
3020                        { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
3021                        { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
3022                        { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
3023                        { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
3024                        { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
3025                    ]
3026                }
3027            }
3028        });
3029
3030        let cache = build_completion_cache(&sources, None, None);
3031        let map = &cache.top_level_importables_by_name;
3032        let by_file = &cache.top_level_importables_by_file;
3033
3034        assert!(map.contains_key("C"));
3035        assert!(map.contains_key("S"));
3036        assert!(map.contains_key("E"));
3037        assert!(map.contains_key("Wad"));
3038        assert!(map.contains_key("freeFn"));
3039        assert!(map.contains_key("TOP_CONST"));
3040
3041        assert!(!map.contains_key("Alias"));
3042        assert!(!map.contains_key("inside"));
3043        assert!(!map.contains_key("TOP_VAR"));
3044
3045        let file_symbols = by_file.get("/tmp/A.sol").unwrap();
3046        let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
3047        assert!(file_names.contains(&"C"));
3048        assert!(file_names.contains(&"TOP_CONST"));
3049        assert!(!file_names.contains(&"Alias"));
3050    }
3051
3052    fn start_at_sources() -> serde_json::Value {
3053        json!({
3054            "/tmp/A.sol": {
3055                "id": 0,
3056                "ast": {
3057                    "id": 1,
3058                    "nodeType": "SourceUnit",
3059                    "src": "0:200:0",
3060                    "nodes": [
3061                        {
3062                            "id": 10,
3063                            "nodeType": "StructDefinition",
3064                            "name": "Contract",
3065                            "scope": 1,
3066                            "src": "0:30:0",
3067                            "members": [
3068                                {
3069                                    "id": 11,
3070                                    "nodeType": "VariableDeclaration",
3071                                    "name": "startAt",
3072                                    "scope": 10,
3073                                    "src": "10:7:0",
3074                                    "typeDescriptions": {
3075                                        "typeString": "uint32",
3076                                        "typeIdentifier": "t_uint32"
3077                                    }
3078                                }
3079                            ]
3080                        },
3081                        {
3082                            "id": 30,
3083                            "nodeType": "EventDefinition",
3084                            "name": "Started",
3085                            "scope": 1,
3086                            "src": "31:20:0",
3087                            "parameters": {
3088                                "id": 31,
3089                                "nodeType": "ParameterList",
3090                                "src": "40:10:0",
3091                                "parameters": [
3092                                    {
3093                                        "id": 32,
3094                                        "nodeType": "VariableDeclaration",
3095                                        "name": "startAt",
3096                                        "scope": 30,
3097                                        "src": "40:7:0",
3098                                        "typeDescriptions": {
3099                                            "typeString": "uint32",
3100                                            "typeIdentifier": "t_uint32"
3101                                        }
3102                                    }
3103                                ]
3104                            }
3105                        },
3106                        {
3107                            "id": 40,
3108                            "nodeType": "ContractDefinition",
3109                            "name": "Pool",
3110                            "scope": 1,
3111                            "src": "60:140:0",
3112                            "nodes": [
3113                                {
3114                                    "id": 41,
3115                                    "nodeType": "VariableDeclaration",
3116                                    "name": "stateVar",
3117                                    "scope": 40,
3118                                    "src": "70:8:0",
3119                                    "typeDescriptions": {
3120                                        "typeString": "uint256",
3121                                        "typeIdentifier": "t_uint256"
3122                                    }
3123                                },
3124                                {
3125                                    "id": 50,
3126                                    "nodeType": "FunctionDefinition",
3127                                    "name": "onStart",
3128                                    "scope": 40,
3129                                    "src": "90:100:0",
3130                                    "parameters": {
3131                                        "id": 49,
3132                                        "nodeType": "ParameterList",
3133                                        "src": "95:12:0",
3134                                        "parameters": [
3135                                            {
3136                                                "id": 51,
3137                                                "nodeType": "VariableDeclaration",
3138                                                "name": "startAt",
3139                                                "scope": 50,
3140                                                "src": "100:7:0",
3141                                                "typeDescriptions": {
3142                                                    "typeString": "uint32",
3143                                                    "typeIdentifier": "t_uint32"
3144                                                }
3145                                            }
3146                                        ]
3147                                    },
3148                                    "body": {
3149                                        "id": 60,
3150                                        "nodeType": "Block",
3151                                        "scope": 50,
3152                                        "src": "110:70:0"
3153                                    }
3154                                }
3155                            ]
3156                        }
3157                    ]
3158                }
3159            }
3160        })
3161    }
3162
3163    fn labels_of(resp: Option<CompletionResponse>) -> Vec<String> {
3164        match resp {
3165            Some(CompletionResponse::List(list)) => {
3166                list.items.into_iter().map(|item| item.label).collect()
3167            }
3168            Some(CompletionResponse::Array(items)) => {
3169                items.into_iter().map(|item| item.label).collect()
3170            }
3171            None => Vec::new(),
3172        }
3173    }
3174
3175    #[test]
3176    fn general_completions_omit_struct_fields_and_params() {
3177        let cache = build_completion_cache(&start_at_sources(), None, None);
3178        let labels: Vec<&str> = cache
3179            .general_completions
3180            .iter()
3181            .map(|item| item.label.as_str())
3182            .collect();
3183
3184        assert!(labels.contains(&"Contract"));
3185        assert!(labels.contains(&"Pool"));
3186        assert!(labels.contains(&"onStart"));
3187        assert!(labels.contains(&"Started"));
3188        assert!(labels.contains(&"stateVar"));
3189        assert!(!labels.contains(&"startAt"));
3190        // Hidden from the general list, but still typed: chains through a
3191        // struct field (`obj.startAt.`) resolve the member through `name_to_type`.
3192        assert!(cache.name_to_type.contains_key("startAt"));
3193
3194        let members = cache.node_members.get(&crate::types::NodeId(10)).unwrap();
3195        assert!(members.iter().any(|item| item.label == "startAt"));
3196    }
3197
3198    #[test]
3199    fn param_completion_only_inside_its_function() {
3200        let cache = build_completion_cache(&start_at_sources(), None, None);
3201        let source = " ".repeat(200);
3202        let outside = labels_of(handle_completion(
3203            Some(&cache),
3204            &source,
3205            Position {
3206                line: 0,
3207                character: 10,
3208            },
3209            None,
3210            Some(FileId(0)),
3211        ));
3212        let inside = labels_of(handle_completion(
3213            Some(&cache),
3214            &source,
3215            Position {
3216                line: 0,
3217                character: 120,
3218            },
3219            None,
3220            Some(FileId(0)),
3221        ));
3222
3223        assert!(!outside.iter().any(|label| label == "startAt"));
3224        assert!(inside.iter().any(|label| label == "startAt"));
3225    }
3226
3227    #[test]
3228    fn member_completion_without_trigger_character() {
3229        let sources = json!({
3230            "/tmp/A.sol": {
3231                "id": 0,
3232                "ast": {
3233                    "id": 1,
3234                    "nodeType": "SourceUnit",
3235                    "src": "0:80:0",
3236                    "nodes": [
3237                        {
3238                            "id": 10,
3239                            "nodeType": "StructDefinition",
3240                            "name": "Info",
3241                            "scope": 1,
3242                            "src": "0:40:0",
3243                            "members": [
3244                                {
3245                                    "id": 11,
3246                                    "nodeType": "VariableDeclaration",
3247                                    "name": "startAt",
3248                                    "scope": 10,
3249                                    "src": "10:7:0",
3250                                    "typeDescriptions": {
3251                                        "typeString": "uint32",
3252                                        "typeIdentifier": "t_uint32"
3253                                    }
3254                                }
3255                            ],
3256                            "typeDescriptions": {
3257                                "typeIdentifier": "t_struct$_Info_$10_storage"
3258                            }
3259                        },
3260                        {
3261                            "id": 40,
3262                            "nodeType": "ContractDefinition",
3263                            "name": "Pool",
3264                            "scope": 1,
3265                            "src": "40:40:0",
3266                            "nodes": [
3267                                {
3268                                    "id": 41,
3269                                    "nodeType": "VariableDeclaration",
3270                                    "name": "obj",
3271                                    "scope": 40,
3272                                    "src": "50:10:0",
3273                                    "typeDescriptions": {
3274                                        "typeString": "struct Info",
3275                                        "typeIdentifier": "t_struct$_Info_$10_storage"
3276                                    }
3277                                },
3278                                {
3279                                    "id": 50,
3280                                    "nodeType": "FunctionDefinition",
3281                                    "name": "run",
3282                                    "scope": 40,
3283                                    "src": "60:20:0",
3284                                    "body": {
3285                                        "id": 60,
3286                                        "nodeType": "Block",
3287                                        "scope": 50,
3288                                        "src": "60:20:0"
3289                                    }
3290                                }
3291                            ]
3292                        }
3293                    ]
3294                }
3295            }
3296        });
3297        let cache = build_completion_cache(&sources, None, None);
3298        // Cursor is on `obj.start`, and the editor did not send trigger `.`.
3299        let source = format!("{:80}", "obj.start");
3300        let labels = labels_of(handle_completion(
3301            Some(&cache),
3302            &source,
3303            Position {
3304                line: 0,
3305                character: "obj.start".len() as u32,
3306            },
3307            None,
3308            Some(FileId(0)),
3309        ));
3310        assert!(
3311            labels.iter().any(|label| label == "startAt"),
3312            "fields missing: {labels:?}"
3313        );
3314    }
3315
3316    #[test]
3317    fn top_level_importables_keep_multiple_declarations_for_same_name() {
3318        let sources = json!({
3319            "/tmp/A.sol": {
3320                "id": 0,
3321                "ast": {
3322                    "id": 1,
3323                    "nodeType": "SourceUnit",
3324                    "src": "0:100:0",
3325                    "nodes": [
3326                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
3327                    ]
3328                }
3329            },
3330            "/tmp/B.sol": {
3331                "id": 1,
3332                "ast": {
3333                    "id": 2,
3334                    "nodeType": "SourceUnit",
3335                    "src": "0:100:1",
3336                    "nodes": [
3337                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
3338                    ]
3339                }
3340            }
3341        });
3342
3343        let cache = build_completion_cache(&sources, None, None);
3344        let entries = cache.top_level_importables_by_name.get("dup").unwrap();
3345        assert_eq!(entries.len(), 2);
3346    }
3347
3348    #[test]
3349    fn extract_top_level_importables_for_file_finds_expected_symbols() {
3350        let ast = json!({
3351            "id": 1,
3352            "nodeType": "SourceUnit",
3353            "src": "0:100:0",
3354            "nodes": [
3355                { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
3356                { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
3357                { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
3358            ]
3359        });
3360
3361        let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
3362        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3363        assert!(names.contains(&"f"));
3364        assert!(names.contains(&"K"));
3365        assert!(!names.contains(&"V"));
3366    }
3367
3368    #[test]
3369    fn top_level_importables_can_be_replaced_per_file() {
3370        let sources = json!({
3371            "/tmp/A.sol": {
3372                "id": 0,
3373                "ast": {
3374                    "id": 1,
3375                    "nodeType": "SourceUnit",
3376                    "src": "0:100:0",
3377                    "nodes": [
3378                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
3379                    ]
3380                }
3381            },
3382            "/tmp/B.sol": {
3383                "id": 1,
3384                "ast": {
3385                    "id": 2,
3386                    "nodeType": "SourceUnit",
3387                    "src": "0:100:1",
3388                    "nodes": [
3389                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
3390                    ]
3391                }
3392            }
3393        });
3394
3395        let mut cache = build_completion_cache(&sources, None, None);
3396        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
3397
3398        cache.replace_top_level_importables_for_path(
3399            "/tmp/A.sol".to_string(),
3400            vec![TopLevelImportable {
3401                name: "newA".to_string(),
3402                declaring_path: "/tmp/A.sol".to_string(),
3403                node_type: "FunctionDefinition".to_string(),
3404                kind: CompletionItemKind::FUNCTION,
3405            }],
3406        );
3407        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
3408        assert!(cache.top_level_importables_by_name.contains_key("newA"));
3409
3410        cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
3411        assert!(!cache.top_level_importables_by_name.contains_key("newA"));
3412    }
3413
3414    #[test]
3415    fn append_auto_import_candidates_last_sets_tail_sort_text() {
3416        let base = vec![CompletionItem {
3417            label: "localVar".to_string(),
3418            ..Default::default()
3419        }];
3420        let auto = vec![CompletionItem {
3421            label: "ImportMe".to_string(),
3422            ..Default::default()
3423        }];
3424
3425        let out = append_auto_import_candidates_last(base, auto);
3426        assert_eq!(out.len(), 2);
3427        assert_eq!(out[1].label, "ImportMe");
3428        assert!(
3429            out[1]
3430                .sort_text
3431                .as_deref()
3432                .is_some_and(|s| s.starts_with("zz_autoimport_"))
3433        );
3434    }
3435
3436    #[test]
3437    fn append_auto_import_candidates_last_keeps_same_label_candidates() {
3438        let base = vec![CompletionItem {
3439            label: "B".to_string(),
3440            ..Default::default()
3441        }];
3442        let auto = vec![
3443            CompletionItem {
3444                label: "B".to_string(),
3445                detail: Some("ContractDefinition (./B.sol)".to_string()),
3446                ..Default::default()
3447            },
3448            CompletionItem {
3449                label: "B".to_string(),
3450                detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
3451                ..Default::default()
3452            },
3453        ];
3454
3455        let out = append_auto_import_candidates_last(base, auto);
3456        assert_eq!(out.len(), 3);
3457    }
3458
3459    #[test]
3460    fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
3461        let base = vec![CompletionItem {
3462            label: "B".to_string(),
3463            ..Default::default()
3464        }];
3465        let auto = vec![CompletionItem {
3466            label: "B".to_string(),
3467            additional_text_edits: Some(vec![TextEdit {
3468                range: Range {
3469                    start: Position {
3470                        line: 0,
3471                        character: 0,
3472                    },
3473                    end: Position {
3474                        line: 0,
3475                        character: 0,
3476                    },
3477                },
3478                new_text: "import {B} from \"./B.sol\";\n".to_string(),
3479            }]),
3480            ..Default::default()
3481        }];
3482        let out = append_auto_import_candidates_last(base, auto);
3483        assert!(
3484            out[0].additional_text_edits.is_some(),
3485            "base item should inherit unique import edit"
3486        );
3487    }
3488
3489    #[test]
3490    fn top_level_importable_candidates_include_import_edit() {
3491        let mut cache = empty_cache();
3492        cache.top_level_importables_by_name.insert(
3493            SymbolName::new("B"),
3494            vec![TopLevelImportable {
3495                name: "B".to_string(),
3496                declaring_path: "/tmp/example/B.sol".to_string(),
3497                node_type: "ContractDefinition".to_string(),
3498                kind: CompletionItemKind::CLASS,
3499            }],
3500        );
3501
3502        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
3503        let items = super::top_level_importable_completion_candidates(
3504            &cache,
3505            Some("/tmp/example/A.sol"),
3506            source,
3507        );
3508        assert_eq!(items.len(), 1);
3509        let edit_text = items[0]
3510            .additional_text_edits
3511            .as_ref()
3512            .and_then(|edits| edits.first())
3513            .map(|e| e.new_text.clone())
3514            .unwrap_or_default();
3515        assert!(edit_text.contains("import {B} from \"./B.sol\";"));
3516    }
3517
3518    #[test]
3519    fn handle_completion_general_path_keeps_base_items() {
3520        let mut cache = empty_cache();
3521        cache.general_completions = vec![CompletionItem {
3522            label: "A".to_string(),
3523            ..Default::default()
3524        }];
3525
3526        let resp = super::handle_completion(
3527            Some(&cache),
3528            "contract X {}",
3529            Position {
3530                line: 0,
3531                character: 0,
3532            },
3533            None,
3534            None,
3535        );
3536        match resp {
3537            Some(CompletionResponse::List(list)) => {
3538                assert_eq!(list.items.len(), 1);
3539                assert_eq!(list.items[0].label, "A");
3540            }
3541            _ => panic!("expected completion list"),
3542        }
3543    }
3544
3545    // --- import path completion tests ---
3546
3547    #[test]
3548    fn make_import_item_has_filter_text_and_text_edit() {
3549        let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
3550        assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
3551        assert!(item.text_edit.is_some(), "text_edit should be set");
3552        assert!(
3553            item.insert_text.is_none(),
3554            "insert_text should be absent when text_edit is set"
3555        );
3556    }
3557
3558    #[test]
3559    fn all_sol_import_paths_no_panic_missing_dir() {
3560        let current = std::path::Path::new("/nonexistent/src/Foo.sol");
3561        let root = std::path::Path::new("/nonexistent");
3562        let items = super::all_sol_import_paths(current, root, &[], None);
3563        assert!(items.is_empty());
3564    }
3565
3566    #[test]
3567    fn all_sol_import_paths_relative_labels() {
3568        // Use real paths from the repo fixture
3569        let root = std::path::Path::new("example");
3570        let current = root.join("A.sol");
3571        let items = super::all_sol_import_paths(&current, root, &[], None);
3572        // All labels should start with ./ or ../
3573        for item in &items {
3574            assert!(
3575                item.label.starts_with("./") || item.label.starts_with("../"),
3576                "label should be relative: {}",
3577                item.label
3578            );
3579            assert!(
3580                item.label.ends_with(".sol"),
3581                "label should end with .sol: {}",
3582                item.label
3583            );
3584        }
3585        assert!(!items.is_empty(), "should find at least one .sol file");
3586    }
3587
3588    /// Real solc 0.8.26 AST for a library-nested enum. The `EnumDefinition`
3589    /// carries no `typeDescriptions`, so `RafluxTypes.ListingStatus.` must
3590    /// resolve the enum through the library's members.
3591    fn nested_enum_sources() -> serde_json::Value {
3592        let ast: serde_json::Value = serde_json::from_str(NESTED_ENUM_AST).unwrap();
3593        json!({ "E.sol": { "id": 0, "ast": ast } })
3594    }
3595
3596    const NESTED_ENUM_AST: &str = r#"{"absolutePath":"E.sol","exportedSymbols":{"C":[18],"RafluxTypes":[6]},"id":19,"license":"MIT","nodeType":"SourceUnit","nodes":[{"id":1,"literals":["solidity","^","0.8",".0"],"nodeType":"PragmaDirective","src":"32:23:0"},{"abstract":false,"baseContracts":[],"canonicalName":"RafluxTypes","contractDependencies":[],"contractKind":"library","fullyImplemented":true,"id":6,"linearizedBaseContracts":[6],"name":"RafluxTypes","nameLocation":"64:11:0","nodeType":"ContractDefinition","nodes":[{"canonicalName":"RafluxTypes.ListingStatus","id":5,"members":[{"id":2,"name":"NONE","nameLocation":"103:4:0","nodeType":"EnumValue","src":"103:4:0"},{"id":3,"name":"ACTIVE","nameLocation":"109:6:0","nodeType":"EnumValue","src":"109:6:0"},{"id":4,"name":"SETTLED","nameLocation":"117:7:0","nodeType":"EnumValue","src":"117:7:0"}],"name":"ListingStatus","nameLocation":"87:13:0","nodeType":"EnumDefinition","src":"82:44:0"}],"scope":19,"src":"56:72:0","usedErrors":[],"usedEvents":[]},{"abstract":false,"baseContracts":[],"canonicalName":"C","contractDependencies":[],"contractKind":"contract","fullyImplemented":true,"id":18,"linearizedBaseContracts":[18],"name":"C","nameLocation":"138:1:0","nodeType":"ContractDefinition","nodes":[{"body":{"id":16,"nodeType":"Block","src":"209:56:0","statements":[{"expression":{"expression":{"expression":{"id":12,"name":"RafluxTypes","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":6,"src":"226:11:0","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_RafluxTypes_$6_$","typeString":"type(library RafluxTypes)"}},"id":13,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"238:13:0","memberName":"ListingStatus","nodeType":"MemberAccess","referencedDeclaration":5,"src":"226:25:0","typeDescriptions":{"typeIdentifier":"t_type$_t_enum$_ListingStatus_$5_$","typeString":"type(enum RafluxTypes.ListingStatus)"}},"id":14,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"memberLocation":"252:6:0","memberName":"ACTIVE","nodeType":"MemberAccess","referencedDeclaration":3,"src":"226:32:0","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"}},"functionReturnParameters":11,"id":15,"nodeType":"Return","src":"219:39:0"}]},"functionSelector":"26121ff0","id":17,"implemented":true,"kind":"function","modifiers":[],"name":"f","nameLocation":"155:1:0","nodeType":"FunctionDefinition","parameters":{"id":7,"nodeType":"ParameterList","parameters":[],"src":"156:2:0"},"returnParameters":{"id":11,"nodeType":"ParameterList","parameters":[{"constant":false,"id":10,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":17,"src":"182:25:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"},"typeName":{"id":9,"nodeType":"UserDefinedTypeName","pathNode":{"id":8,"name":"RafluxTypes.ListingStatus","nameLocations":["182:11:0","194:13:0"],"nodeType":"IdentifierPath","referencedDeclaration":5,"src":"182:25:0"},"referencedDeclaration":5,"src":"182:25:0","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"}},"visibility":"internal"}],"src":"181:27:0"},"scope":18,"src":"146:119:0","stateMutability":"pure","virtual":false,"visibility":"external"}],"scope":19,"src":"129:138:0","usedErrors":[],"usedEvents":[]}],"src":"32:236:0"}"#;
3597
3598    #[test]
3599    fn nested_enum_chain_completes_enum_values() {
3600        let cache = build_completion_cache(&nested_enum_sources(), None, None);
3601        let chain = crate::completion::parse_dot_chain("        RafluxTypes.ListingStatus.", 34);
3602        let labels: Vec<String> = crate::completion::get_chain_completions(&cache, &chain, None)
3603            .into_iter()
3604            .map(|c| c.label)
3605            .collect();
3606        assert_eq!(labels, vec!["NONE", "ACTIVE", "SETTLED"]);
3607    }
3608}