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