1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use tower_lsp::lsp_types::{
5 CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, CompletionTextEdit,
6 Position, Range, TextEdit,
7};
8
9use crate::goto::CHILD_KEYS;
10use crate::hover::build_function_signature;
11use crate::types::{FileId, NodeId, RelPath, SourceLoc, SymbolName, TypeIdentifier};
12use crate::utils::push_if_node_or_array;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct TopLevelImportable {
17 pub name: String,
19 pub declaring_path: String,
21 pub node_type: String,
23 pub kind: CompletionItemKind,
25}
26
27#[derive(Debug, Clone)]
29pub struct ScopedDeclaration {
30 pub name: String,
32 pub type_id: String,
34}
35
36#[derive(Debug, Clone)]
38pub struct ScopeRange {
39 pub node_id: NodeId,
41 pub start: usize,
43 pub end: usize,
45 pub file_id: FileId,
47}
48
49#[derive(Debug)]
51pub struct CompletionCache {
52 pub names: Vec<CompletionItem>,
54
55 pub name_to_type: HashMap<SymbolName, TypeIdentifier>,
57
58 pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
60
61 pub type_to_node: HashMap<TypeIdentifier, NodeId>,
63
64 pub name_to_node_id: HashMap<SymbolName, NodeId>,
66
67 pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
70
71 pub function_return_types: HashMap<(NodeId, String), String>,
74
75 pub using_for: HashMap<TypeIdentifier, Vec<CompletionItem>>,
78
79 pub using_for_wildcard: Vec<CompletionItem>,
81
82 pub general_completions: Vec<CompletionItem>,
85
86 pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
90
91 pub scope_parent: HashMap<NodeId, NodeId>,
94
95 pub scope_ranges: Vec<ScopeRange>,
98
99 pub path_to_file_id: HashMap<RelPath, FileId>,
102
103 pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
107
108 pub contract_kinds: HashMap<NodeId, String>,
112
113 pub top_level_importables_by_name: HashMap<SymbolName, Vec<TopLevelImportable>>,
119
120 pub top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>>,
125}
126
127fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
129 match node_type {
130 "FunctionDefinition" => CompletionItemKind::FUNCTION,
131 "VariableDeclaration" => CompletionItemKind::VARIABLE,
132 "ContractDefinition" => CompletionItemKind::CLASS,
133 "StructDefinition" => CompletionItemKind::STRUCT,
134 "EnumDefinition" => CompletionItemKind::ENUM,
135 "EnumValue" => CompletionItemKind::ENUM_MEMBER,
136 "EventDefinition" => CompletionItemKind::EVENT,
137 "ErrorDefinition" => CompletionItemKind::EVENT,
138 "ModifierDefinition" => CompletionItemKind::METHOD,
139 "ImportDirective" => CompletionItemKind::MODULE,
140 _ => CompletionItemKind::TEXT,
141 }
142}
143
144fn parse_src(node: &Value) -> Option<SourceLoc> {
147 let src = node.get("src").and_then(|v| v.as_str())?;
148 SourceLoc::parse(src)
149}
150
151pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
156 let mut last_id = None;
159 let mut i = 0;
160 let bytes = type_id.as_bytes();
161 while i < bytes.len() {
162 if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
163 i += 2;
164 let start = i;
165 while i < bytes.len() && bytes[i].is_ascii_digit() {
166 i += 1;
167 }
168 if i > start
169 && let Ok(id) = type_id[start..i].parse::<i64>()
170 {
171 last_id = Some(NodeId(id));
172 }
173 } else {
174 i += 1;
175 }
176 }
177 last_id
178}
179
180pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
187 let mut current = type_id;
188
189 loop {
190 if !current.starts_with("t_mapping$_") {
191 let result = current.trim_end_matches("_$");
194 return if result.is_empty() {
195 None
196 } else {
197 Some(result.to_string())
198 };
199 }
200
201 let inner = ¤t["t_mapping$_".len()..];
203
204 let mut depth = 0i32;
208 let bytes = inner.as_bytes();
209 let mut split_pos = None;
210
211 let mut i = 0;
212 while i < bytes.len() {
213 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
214 depth += 1;
215 i += 2;
216 } else if i + 2 < bytes.len()
217 && bytes[i] == b'_'
218 && bytes[i + 1] == b'$'
219 && bytes[i + 2] == b'_'
220 && depth == 0
221 {
222 split_pos = Some(i);
224 break;
225 } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
226 depth -= 1;
227 i += 2;
228 } else {
229 i += 1;
230 }
231 }
232
233 if let Some(pos) = split_pos {
234 current = &inner[pos + 3..];
236 } else {
237 return None;
238 }
239 }
240}
241
242fn count_abi_params(signature: &str) -> usize {
245 let start = match signature.find('(') {
247 Some(i) => i + 1,
248 None => return 0,
249 };
250 let bytes = signature.as_bytes();
251 if start >= bytes.len() {
252 return 0;
253 }
254 if bytes[start] == b')' {
256 return 0;
257 }
258 let mut count = 1; let mut depth = 0;
260 for &b in &bytes[start..] {
261 match b {
262 b'(' => depth += 1,
263 b')' => {
264 if depth == 0 {
265 break;
266 }
267 depth -= 1;
268 }
269 b',' if depth == 0 => count += 1,
270 _ => {}
271 }
272 }
273 count
274}
275
276fn count_signature_params(sig: &str) -> usize {
278 count_abi_params(sig)
279}
280
281fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
282 match node_type {
283 "ContractDefinition"
284 | "StructDefinition"
285 | "EnumDefinition"
286 | "UserDefinedValueTypeDefinition"
287 | "FunctionDefinition" => true,
288 "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
289 _ => false,
290 }
291}
292
293fn build_top_level_importables_by_name(
294 by_file: &HashMap<RelPath, Vec<TopLevelImportable>>,
295) -> HashMap<SymbolName, Vec<TopLevelImportable>> {
296 let mut by_name: HashMap<SymbolName, Vec<TopLevelImportable>> = HashMap::new();
297 for symbols in by_file.values() {
298 for symbol in symbols {
299 by_name
300 .entry(SymbolName::new(symbol.name.clone()))
301 .or_default()
302 .push(symbol.clone());
303 }
304 }
305 by_name
306}
307
308pub fn extract_top_level_importables_for_file(path: &str, ast: &Value) -> Vec<TopLevelImportable> {
313 let mut out: Vec<TopLevelImportable> = Vec::new();
314 let mut stack: Vec<&Value> = vec![ast];
315 let mut source_unit_id: Option<NodeId> = None;
316
317 while let Some(tree) = stack.pop() {
318 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
319 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
320 if node_type == "SourceUnit" {
321 source_unit_id = node_id;
322 }
323 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
324
325 if !name.is_empty()
326 && is_top_level_importable_decl(node_type, tree)
327 && let Some(src_scope) = source_unit_id
328 && tree.get("scope").and_then(|v| v.as_i64()) == Some(src_scope.0)
329 {
330 out.push(TopLevelImportable {
331 name: name.to_string(),
332 declaring_path: path.to_string(),
333 node_type: node_type.to_string(),
334 kind: node_type_to_completion_kind(node_type),
335 });
336 }
337
338 for key in CHILD_KEYS {
339 push_if_node_or_array(tree, key, &mut stack);
340 }
341 }
342
343 out
344}
345
346impl CompletionCache {
347 pub fn replace_top_level_importables_for_path(
350 &mut self,
351 path: String,
352 symbols: Vec<TopLevelImportable>,
353 ) {
354 self.top_level_importables_by_file
355 .insert(RelPath::new(path), symbols);
356 self.top_level_importables_by_name =
357 build_top_level_importables_by_name(&self.top_level_importables_by_file);
358 }
359}
360
361pub fn build_completion_cache(
369 sources: &Value,
370 contracts: Option<&Value>,
371 file_id_remap: Option<&HashMap<u64, FileId>>,
372) -> CompletionCache {
373 let source_count = sources.as_object().map_or(0, |obj| obj.len());
374 let est_names = source_count * 20;
377 let est_contracts = source_count * 5;
378
379 let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
380 let mut seen_names: HashMap<SymbolName, usize> = HashMap::with_capacity(est_names);
381 let mut name_to_type: HashMap<SymbolName, TypeIdentifier> = HashMap::with_capacity(est_names);
382 let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
383 HashMap::with_capacity(est_contracts);
384 let mut type_to_node: HashMap<TypeIdentifier, NodeId> = HashMap::with_capacity(est_contracts);
385 let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
386 HashMap::with_capacity(est_contracts);
387 let mut name_to_node_id: HashMap<SymbolName, NodeId> = HashMap::with_capacity(est_names);
388 let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
389
390 let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
392
393 let mut function_signatures: HashMap<NodeId, HashMap<SymbolName, Vec<String>>> =
395 HashMap::with_capacity(est_contracts);
396
397 let mut function_return_types: HashMap<(NodeId, String), String> =
399 HashMap::with_capacity(source_count * 10);
400
401 let mut using_for: HashMap<TypeIdentifier, Vec<CompletionItem>> =
403 HashMap::with_capacity(source_count);
404 let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
405
406 let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
408
409 let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
411 HashMap::with_capacity(est_contracts);
412 let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
413 let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
414 let mut path_to_file_id: HashMap<RelPath, FileId> = HashMap::with_capacity(source_count);
415 let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
416 HashMap::with_capacity(est_contracts);
417 let mut top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>> =
418 HashMap::with_capacity(est_names);
419
420 if let Some(sources_obj) = sources.as_object() {
421 for (path, source_data) in sources_obj {
422 if let Some(ast) = source_data.get("ast") {
423 if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
427 let canonical_fid = file_id_remap
428 .and_then(|r| r.get(&fid).copied())
429 .unwrap_or(FileId(fid));
430 path_to_file_id.insert(RelPath::new(path), canonical_fid);
431 }
432 let file_importables = extract_top_level_importables_for_file(path, ast);
433 if !file_importables.is_empty() {
434 top_level_importables_by_file.insert(RelPath::new(path), file_importables);
435 }
436 let mut stack: Vec<&Value> = vec![ast];
437
438 while let Some(tree) = stack.pop() {
439 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
440 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
441 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
442
443 let is_scope_node = matches!(
448 node_type,
449 "SourceUnit"
450 | "ContractDefinition"
451 | "FunctionDefinition"
452 | "ModifierDefinition"
453 | "Block"
454 | "UncheckedBlock"
455 );
456 if is_scope_node && let Some(nid) = node_id {
457 if let Some(src_loc) = parse_src(tree) {
458 let canonical_fid = file_id_remap
459 .and_then(|r| r.get(&src_loc.file_id.0).copied())
460 .unwrap_or(src_loc.file_id);
461 scope_ranges.push(ScopeRange {
462 node_id: nid,
463 start: src_loc.offset,
464 end: src_loc.end(),
465 file_id: canonical_fid,
466 });
467 }
468 if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_i64()) {
470 scope_parent.insert(nid, NodeId(parent_id));
471 }
472 }
473
474 if node_type == "ContractDefinition"
476 && let Some(nid) = node_id
477 && let Some(bases) = tree
478 .get("linearizedBaseContracts")
479 .and_then(|v| v.as_array())
480 {
481 let base_ids: Vec<NodeId> = bases
482 .iter()
483 .filter_map(|b| b.as_i64())
484 .map(NodeId)
485 .collect();
486 if !base_ids.is_empty() {
487 linearized_base_contracts.insert(nid, base_ids);
488 }
489 }
490
491 if node_type == "VariableDeclaration"
493 && !name.is_empty()
494 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
495 && let Some(tid) = tree
496 .get("typeDescriptions")
497 .and_then(|td| td.get("typeIdentifier"))
498 .and_then(|v| v.as_str())
499 {
500 scope_declarations
501 .entry(NodeId(scope_raw))
502 .or_default()
503 .push(ScopedDeclaration {
504 name: name.to_string(),
505 type_id: tid.to_string(),
506 });
507 }
508
509 if node_type == "FunctionDefinition"
511 && !name.is_empty()
512 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
513 && let Some(tid) = tree
514 .get("typeDescriptions")
515 .and_then(|td| td.get("typeIdentifier"))
516 .and_then(|v| v.as_str())
517 {
518 scope_declarations
519 .entry(NodeId(scope_raw))
520 .or_default()
521 .push(ScopedDeclaration {
522 name: name.to_string(),
523 type_id: tid.to_string(),
524 });
525 }
526
527 if !name.is_empty() && !seen_names.contains_key(name) {
529 let type_string = tree
530 .get("typeDescriptions")
531 .and_then(|td| td.get("typeString"))
532 .and_then(|v| v.as_str())
533 .map(|s| s.to_string());
534
535 let type_id = tree
536 .get("typeDescriptions")
537 .and_then(|td| td.get("typeIdentifier"))
538 .and_then(|v| v.as_str());
539
540 let kind = node_type_to_completion_kind(node_type);
541
542 let item = CompletionItem {
543 label: name.to_string(),
544 kind: Some(kind),
545 detail: type_string,
546 ..Default::default()
547 };
548
549 let idx = names.len();
550 names.push(item);
551 seen_names.insert(SymbolName::new(name), idx);
552
553 if let Some(tid) = type_id {
555 name_to_type.insert(SymbolName::new(name), TypeIdentifier::new(tid));
556 }
557 }
558
559 if node_type == "StructDefinition"
561 && let Some(id) = node_id
562 {
563 let mut members = Vec::new();
564 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
565 for member in member_array {
566 let member_name =
567 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
568 if member_name.is_empty() {
569 continue;
570 }
571 let member_type = member
572 .get("typeDescriptions")
573 .and_then(|td| td.get("typeString"))
574 .and_then(|v| v.as_str())
575 .map(|s| s.to_string());
576
577 members.push(CompletionItem {
578 label: member_name.to_string(),
579 kind: Some(CompletionItemKind::FIELD),
580 detail: member_type,
581 ..Default::default()
582 });
583 }
584 }
585 if !members.is_empty() {
586 node_members.insert(id, members);
587 }
588
589 if let Some(tid) = tree
591 .get("typeDescriptions")
592 .and_then(|td| td.get("typeIdentifier"))
593 .and_then(|v| v.as_str())
594 {
595 type_to_node.insert(TypeIdentifier::new(tid), id);
596 }
597 }
598
599 if node_type == "ContractDefinition"
601 && let Some(id) = node_id
602 {
603 let mut members = Vec::new();
604 let mut fn_sigs: HashMap<SymbolName, Vec<String>> = HashMap::new();
605 if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
606 for member in nodes_array {
607 let member_type = member
608 .get("nodeType")
609 .and_then(|v| v.as_str())
610 .unwrap_or("");
611 let member_name =
612 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
613 if member_name.is_empty() {
614 continue;
615 }
616
617 let (member_detail, label_details) =
619 if member_type == "FunctionDefinition" {
620 if let Some(ret_params) = member
624 .get("returnParameters")
625 .and_then(|rp| rp.get("parameters"))
626 .and_then(|v| v.as_array())
627 && ret_params.len() == 1
628 && let Some(ret_tid) = ret_params[0]
629 .get("typeDescriptions")
630 .and_then(|td| td.get("typeIdentifier"))
631 .and_then(|v| v.as_str())
632 {
633 function_return_types.insert(
634 (id, member_name.to_string()),
635 ret_tid.to_string(),
636 );
637 }
638
639 if let Some(sig) = build_function_signature(member) {
640 fn_sigs
641 .entry(SymbolName::new(member_name))
642 .or_default()
643 .push(sig.clone());
644 (Some(sig), None)
645 } else {
646 (
647 member
648 .get("typeDescriptions")
649 .and_then(|td| td.get("typeString"))
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string()),
652 None,
653 )
654 }
655 } else {
656 (
657 member
658 .get("typeDescriptions")
659 .and_then(|td| td.get("typeString"))
660 .and_then(|v| v.as_str())
661 .map(|s| s.to_string()),
662 None,
663 )
664 };
665
666 let kind = node_type_to_completion_kind(member_type);
667 members.push(CompletionItem {
668 label: member_name.to_string(),
669 kind: Some(kind),
670 detail: member_detail,
671 label_details,
672 ..Default::default()
673 });
674 }
675 }
676 if !members.is_empty() {
677 node_members.insert(id, members);
678 }
679 if !fn_sigs.is_empty() {
680 function_signatures.insert(id, fn_sigs);
681 }
682
683 if let Some(tid) = tree
684 .get("typeDescriptions")
685 .and_then(|td| td.get("typeIdentifier"))
686 .and_then(|v| v.as_str())
687 {
688 type_to_node.insert(TypeIdentifier::new(tid), id);
689 }
690
691 if !name.is_empty() {
693 contract_locations.push((path.clone(), name.to_string(), id));
694 name_to_node_id.insert(SymbolName::new(name), id);
695 }
696
697 if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
699 contract_kinds.insert(id, ck.to_string());
700 }
701 }
702
703 if node_type == "EnumDefinition"
705 && let Some(id) = node_id
706 {
707 let mut members = Vec::new();
708 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
709 for member in member_array {
710 let member_name =
711 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
712 if member_name.is_empty() {
713 continue;
714 }
715 members.push(CompletionItem {
716 label: member_name.to_string(),
717 kind: Some(CompletionItemKind::ENUM_MEMBER),
718 detail: None,
719 ..Default::default()
720 });
721 }
722 }
723 if !members.is_empty() {
724 node_members.insert(id, members);
725 }
726
727 if let Some(tid) = tree
728 .get("typeDescriptions")
729 .and_then(|td| td.get("typeIdentifier"))
730 .and_then(|v| v.as_str())
731 {
732 type_to_node.insert(TypeIdentifier::new(tid), id);
733 }
734 }
735
736 if node_type == "UsingForDirective" {
738 let target_type = tree.get("typeName").and_then(|tn| {
740 tn.get("typeDescriptions")
741 .and_then(|td| td.get("typeIdentifier"))
742 .and_then(|v| v.as_str())
743 .map(|s| s.to_string())
744 });
745
746 if let Some(lib) = tree.get("libraryName") {
748 if let Some(lib_id) =
749 lib.get("referencedDeclaration").and_then(|v| v.as_i64())
750 {
751 using_for_directives.push((NodeId(lib_id), target_type));
752 }
753 }
754 else if let Some(func_list) =
758 tree.get("functionList").and_then(|v| v.as_array())
759 {
760 for entry in func_list {
761 if entry.get("operator").is_some() {
763 continue;
764 }
765 if let Some(def) = entry.get("definition") {
766 let fn_name =
767 def.get("name").and_then(|v| v.as_str()).unwrap_or("");
768 if !fn_name.is_empty() {
769 let items = if let Some(ref tid) = target_type {
770 using_for
771 .entry(TypeIdentifier::new(tid.clone()))
772 .or_default()
773 } else {
774 &mut using_for_wildcard
775 };
776 items.push(CompletionItem {
777 label: fn_name.to_string(),
778 kind: Some(CompletionItemKind::FUNCTION),
779 detail: None,
780 ..Default::default()
781 });
782 }
783 }
784 }
785 }
786 }
787
788 for key in CHILD_KEYS {
790 push_if_node_or_array(tree, key, &mut stack);
791 }
792 }
793 }
794 }
795 }
796
797 for (lib_id, target_type) in &using_for_directives {
800 if let Some(lib_members) = node_members.get(lib_id) {
801 let items: Vec<CompletionItem> = lib_members
802 .iter()
803 .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
804 .cloned()
805 .collect();
806 if !items.is_empty() {
807 if let Some(tid) = target_type {
808 using_for
809 .entry(TypeIdentifier::new(tid.clone()))
810 .or_default()
811 .extend(items);
812 } else {
813 using_for_wildcard.extend(items);
814 }
815 }
816 }
817 }
818
819 if let Some(contracts_val) = contracts
821 && let Some(contracts_obj) = contracts_val.as_object()
822 {
823 for (path, contract_name, node_id) in &contract_locations {
824 let fn_sigs = function_signatures.get(node_id);
826
827 if let Some(path_entry) = contracts_obj.get(path)
828 && let Some(contract_entry) = path_entry.get(contract_name)
829 && let Some(evm) = contract_entry.get("evm")
830 && let Some(methods) = evm.get("methodIdentifiers")
831 && let Some(methods_obj) = methods.as_object()
832 {
833 let mut items: Vec<CompletionItem> = Vec::new();
834 for (signature, selector_val) in methods_obj {
835 let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
838 let selector_str = selector_val
839 .as_str()
840 .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
841 .unwrap_or_default();
842
843 let description =
845 fn_sigs
846 .and_then(|sigs| sigs.get(&fn_name))
847 .and_then(|sig_list| {
848 if sig_list.len() == 1 {
849 Some(sig_list[0].clone())
851 } else {
852 let abi_param_count = count_abi_params(signature);
854 sig_list
855 .iter()
856 .find(|s| count_signature_params(s) == abi_param_count)
857 .cloned()
858 }
859 });
860
861 items.push(CompletionItem {
862 label: fn_name,
863 kind: Some(CompletionItemKind::FUNCTION),
864 detail: Some(signature.clone()),
865 label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
866 detail: Some(selector_str),
867 description,
868 }),
869 ..Default::default()
870 });
871 }
872 if !items.is_empty() {
873 method_identifiers.insert(*node_id, items);
874 }
875 }
876 }
877 }
878
879 let mut general_completions = names.clone();
881 general_completions.extend(get_static_completions());
882
883 scope_ranges.sort_by_key(|r| r.end - r.start);
885
886 let orphan_ids: Vec<NodeId> = scope_ranges
892 .iter()
893 .filter(|r| !scope_parent.contains_key(&r.node_id))
894 .map(|r| r.node_id)
895 .collect();
896 let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
898 .iter()
899 .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
900 .collect();
901 for orphan_id in &orphan_ids {
902 if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
903 let parent = scope_ranges
906 .iter()
907 .find(|r| {
908 r.node_id != *orphan_id
909 && r.file_id == file_id
910 && r.start <= start
911 && r.end >= end
912 && (r.end - r.start) > (end - start)
913 })
914 .map(|r| r.node_id);
915 if let Some(parent_id) = parent {
916 scope_parent.insert(*orphan_id, parent_id);
917 }
918 }
919 }
920
921 let top_level_importables_by_name =
922 build_top_level_importables_by_name(&top_level_importables_by_file);
923
924 CompletionCache {
925 names,
926 name_to_type,
927 node_members,
928 type_to_node,
929 name_to_node_id,
930 method_identifiers,
931 function_return_types,
932 using_for,
933 using_for_wildcard,
934 general_completions,
935 scope_declarations,
936 scope_parent,
937 scope_ranges,
938 path_to_file_id,
939 linearized_base_contracts,
940 contract_kinds,
941 top_level_importables_by_name,
942 top_level_importables_by_file,
943 }
944}
945
946fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
948 let items = match name {
949 "msg" => vec![
950 ("data", "bytes calldata"),
951 ("sender", "address"),
952 ("sig", "bytes4"),
953 ("value", "uint256"),
954 ],
955 "block" => vec![
956 ("basefee", "uint256"),
957 ("blobbasefee", "uint256"),
958 ("chainid", "uint256"),
959 ("coinbase", "address payable"),
960 ("difficulty", "uint256"),
961 ("gaslimit", "uint256"),
962 ("number", "uint256"),
963 ("prevrandao", "uint256"),
964 ("timestamp", "uint256"),
965 ],
966 "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
967 "abi" => vec![
968 ("decode(bytes memory, (...))", "..."),
969 ("encode(...)", "bytes memory"),
970 ("encodePacked(...)", "bytes memory"),
971 ("encodeWithSelector(bytes4, ...)", "bytes memory"),
972 ("encodeWithSignature(string memory, ...)", "bytes memory"),
973 ("encodeCall(function, (...))", "bytes memory"),
974 ],
975 "type" => vec![
978 ("name", "string"),
979 ("creationCode", "bytes memory"),
980 ("runtimeCode", "bytes memory"),
981 ("interfaceId", "bytes4"),
982 ("min", "T"),
983 ("max", "T"),
984 ],
985 "bytes" => vec![("concat(...)", "bytes memory")],
987 "string" => vec![("concat(...)", "string memory")],
988 _ => return None,
989 };
990
991 Some(
992 items
993 .into_iter()
994 .map(|(label, detail)| CompletionItem {
995 label: label.to_string(),
996 kind: Some(CompletionItemKind::PROPERTY),
997 detail: Some(detail.to_string()),
998 ..Default::default()
999 })
1000 .collect(),
1001 )
1002}
1003
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1007enum TypeMetaKind {
1008 Contract,
1010 Interface,
1012 IntegerType,
1014 Unknown,
1016}
1017
1018fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
1020 if arg == "int" || arg == "uint" {
1022 return TypeMetaKind::IntegerType;
1023 }
1024 if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1025 && let Ok(n) = suffix.parse::<u16>()
1026 && (8..=256).contains(&n)
1027 && n % 8 == 0
1028 {
1029 return TypeMetaKind::IntegerType;
1030 }
1031
1032 if let Some(c) = cache
1034 && let Some(&node_id) = c.name_to_node_id.get(arg)
1035 {
1036 return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1037 Some("interface") => TypeMetaKind::Interface,
1038 Some("library") => TypeMetaKind::Contract, _ => TypeMetaKind::Contract,
1040 };
1041 }
1042
1043 TypeMetaKind::Unknown
1044}
1045
1046fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1048 let kind = match arg {
1049 Some(a) => classify_type_arg(a, cache),
1050 None => TypeMetaKind::Unknown,
1051 };
1052
1053 let items: Vec<(&str, &str)> = match kind {
1054 TypeMetaKind::Contract => vec![
1055 ("name", "string"),
1056 ("creationCode", "bytes memory"),
1057 ("runtimeCode", "bytes memory"),
1058 ],
1059 TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1060 TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1061 TypeMetaKind::Unknown => vec![
1062 ("name", "string"),
1063 ("creationCode", "bytes memory"),
1064 ("runtimeCode", "bytes memory"),
1065 ("interfaceId", "bytes4"),
1066 ("min", "T"),
1067 ("max", "T"),
1068 ],
1069 };
1070
1071 items
1072 .into_iter()
1073 .map(|(label, detail)| CompletionItem {
1074 label: label.to_string(),
1075 kind: Some(CompletionItemKind::PROPERTY),
1076 detail: Some(detail.to_string()),
1077 ..Default::default()
1078 })
1079 .collect()
1080}
1081
1082fn address_members() -> Vec<CompletionItem> {
1084 [
1085 ("balance", "uint256", CompletionItemKind::PROPERTY),
1086 ("code", "bytes memory", CompletionItemKind::PROPERTY),
1087 ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1088 ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1089 ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1090 (
1091 "call(bytes memory)",
1092 "(bool, bytes memory)",
1093 CompletionItemKind::FUNCTION,
1094 ),
1095 (
1096 "delegatecall(bytes memory)",
1097 "(bool, bytes memory)",
1098 CompletionItemKind::FUNCTION,
1099 ),
1100 (
1101 "staticcall(bytes memory)",
1102 "(bool, bytes memory)",
1103 CompletionItemKind::FUNCTION,
1104 ),
1105 ]
1106 .iter()
1107 .map(|(label, detail, kind)| CompletionItem {
1108 label: label.to_string(),
1109 kind: Some(*kind),
1110 detail: if detail.is_empty() {
1111 None
1112 } else {
1113 Some(detail.to_string())
1114 },
1115 ..Default::default()
1116 })
1117 .collect()
1118}
1119
1120#[derive(Debug, Clone, PartialEq)]
1122pub enum AccessKind {
1123 Plain,
1125 Call,
1127 Index,
1129}
1130
1131#[derive(Debug, Clone, PartialEq)]
1133pub struct DotSegment {
1134 pub name: String,
1135 pub kind: AccessKind,
1136 pub call_args: Option<String>,
1139}
1140
1141fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1145 let close = bytes[pos];
1146 let open = match close {
1147 b')' => b'(',
1148 b']' => b'[',
1149 _ => return pos,
1150 };
1151 let mut depth = 1u32;
1152 let mut i = pos;
1153 while i > 0 && depth > 0 {
1154 i -= 1;
1155 if bytes[i] == close {
1156 depth += 1;
1157 } else if bytes[i] == open {
1158 depth -= 1;
1159 }
1160 }
1161 i
1162}
1163
1164pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1169 let col = character as usize;
1170 if col == 0 {
1171 return vec![];
1172 }
1173
1174 let bytes = line.as_bytes();
1175 let mut segments: Vec<DotSegment> = Vec::new();
1176
1177 let mut pos = col;
1179 if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1180 pos -= 1;
1181 }
1182
1183 loop {
1184 if pos == 0 {
1185 break;
1186 }
1187
1188 let (kind, call_args) = if bytes[pos - 1] == b')' {
1190 let close = pos - 1; pos = skip_brackets_backwards(bytes, close);
1192 let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1194 .trim()
1195 .to_string();
1196 let args = if args_text.is_empty() {
1197 None
1198 } else {
1199 Some(args_text)
1200 };
1201 (AccessKind::Call, args)
1202 } else if bytes[pos - 1] == b']' {
1203 pos -= 1; pos = skip_brackets_backwards(bytes, pos);
1205 (AccessKind::Index, None)
1206 } else {
1207 (AccessKind::Plain, None)
1208 };
1209
1210 let end = pos;
1212 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1213 pos -= 1;
1214 }
1215
1216 if pos == end {
1217 break;
1219 }
1220
1221 let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1222 segments.push(DotSegment {
1223 name,
1224 kind,
1225 call_args,
1226 });
1227
1228 if pos > 0 && bytes[pos - 1] == b'.' {
1230 pos -= 1; } else {
1232 break;
1233 }
1234 }
1235
1236 segments.reverse(); segments
1238}
1239
1240pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1243 let segments = parse_dot_chain(line, character);
1244 segments.last().map(|s| s.name.clone())
1245}
1246
1247#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1248Solidity AST uses different suffixes in different contexts:
1249 - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1250 - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1251 - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1252All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1253fn strip_type_suffix(type_id: &str) -> &str {
1254 let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1255 s.strip_suffix("_storage")
1256 .or_else(|| s.strip_suffix("_memory"))
1257 .or_else(|| s.strip_suffix("_calldata"))
1258 .unwrap_or(s)
1259}
1260
1261fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1265 if let Some(items) = cache.using_for.get(type_id) {
1267 return items.clone();
1268 }
1269
1270 let base = strip_type_suffix(type_id);
1272 let variants = [
1273 base.to_string(),
1274 format!("{}_storage", base),
1275 format!("{}_storage_ptr", base),
1276 format!("{}_memory", base),
1277 format!("{}_memory_ptr", base),
1278 format!("{}_calldata", base),
1279 ];
1280 for variant in &variants {
1281 if variant.as_str() != type_id
1282 && let Some(items) = cache.using_for.get(variant.as_str())
1283 {
1284 return items.clone();
1285 }
1286 }
1287
1288 vec![]
1289}
1290
1291fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1294 if type_id == "t_address" || type_id == "t_address_payable" {
1296 let mut items = address_members();
1297 if let Some(uf) = cache.using_for.get(type_id) {
1299 items.extend(uf.iter().cloned());
1300 }
1301 items.extend(cache.using_for_wildcard.iter().cloned());
1302 return items;
1303 }
1304
1305 let resolved_node_id = extract_node_id_from_type(type_id)
1306 .or_else(|| cache.type_to_node.get(type_id).copied())
1307 .or_else(|| {
1308 type_id
1310 .strip_prefix("__node_id_")
1311 .and_then(|s| s.parse::<i64>().ok())
1312 .map(NodeId)
1313 });
1314
1315 let mut items = Vec::new();
1316 let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1317
1318 if let Some(node_id) = resolved_node_id {
1319 if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1321 for item in method_items {
1322 seen_labels.insert(item.label.clone());
1323 items.push(item.clone());
1324 }
1325 }
1326
1327 if let Some(members) = cache.node_members.get(&node_id) {
1329 for item in members {
1330 if !seen_labels.contains(&item.label) {
1331 seen_labels.insert(item.label.clone());
1332 items.push(item.clone());
1333 }
1334 }
1335 }
1336 }
1337
1338 let is_contract_name = resolved_node_id
1342 .map(|nid| cache.contract_kinds.contains_key(&nid))
1343 .unwrap_or(false);
1344
1345 if !is_contract_name {
1346 let uf_items = lookup_using_for(cache, type_id);
1348 for item in &uf_items {
1349 if !seen_labels.contains(&item.label) {
1350 seen_labels.insert(item.label.clone());
1351 items.push(item.clone());
1352 }
1353 }
1354
1355 for item in &cache.using_for_wildcard {
1357 if !seen_labels.contains(&item.label) {
1358 seen_labels.insert(item.label.clone());
1359 items.push(item.clone());
1360 }
1361 }
1362 }
1363
1364 items
1365}
1366
1367fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1369 if let Some(tid) = cache.name_to_type.get(name) {
1371 return Some(tid.to_string());
1372 }
1373 if let Some(node_id) = cache.name_to_node_id.get(name) {
1375 for (tid, nid) in &cache.type_to_node {
1377 if nid == node_id {
1378 return Some(tid.to_string());
1379 }
1380 }
1381 return Some(format!("__node_id_{}", node_id));
1383 }
1384 None
1385}
1386
1387pub fn find_innermost_scope(
1391 cache: &CompletionCache,
1392 byte_pos: usize,
1393 file_id: FileId,
1394) -> Option<NodeId> {
1395 cache
1397 .scope_ranges
1398 .iter()
1399 .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1400 .map(|r| r.node_id)
1401}
1402
1403pub fn resolve_name_in_scope(
1412 cache: &CompletionCache,
1413 name: &str,
1414 byte_pos: usize,
1415 file_id: FileId,
1416) -> Option<String> {
1417 let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1418
1419 loop {
1421 if let Some(decls) = cache.scope_declarations.get(¤t_scope) {
1423 for decl in decls {
1424 if decl.name == name {
1425 return Some(decl.type_id.clone());
1426 }
1427 }
1428 }
1429
1430 if let Some(bases) = cache.linearized_base_contracts.get(¤t_scope) {
1434 for &base_id in bases.iter().skip(1) {
1435 if let Some(decls) = cache.scope_declarations.get(&base_id) {
1436 for decl in decls {
1437 if decl.name == name {
1438 return Some(decl.type_id.clone());
1439 }
1440 }
1441 }
1442 }
1443 }
1444
1445 match cache.scope_parent.get(¤t_scope) {
1447 Some(&parent_id) => current_scope = parent_id,
1448 None => break, }
1450 }
1451
1452 resolve_name_to_type_id(cache, name)
1455}
1456
1457fn resolve_member_type(
1462 cache: &CompletionCache,
1463 context_type_id: &str,
1464 member_name: &str,
1465 kind: &AccessKind,
1466) -> Option<String> {
1467 let resolved_node_id = extract_node_id_from_type(context_type_id)
1468 .or_else(|| cache.type_to_node.get(context_type_id).copied())
1469 .or_else(|| {
1470 context_type_id
1472 .strip_prefix("__node_id_")
1473 .and_then(|s| s.parse::<i64>().ok())
1474 .map(NodeId)
1475 });
1476
1477 let node_id = resolved_node_id?;
1478
1479 match kind {
1480 AccessKind::Call => {
1481 cache
1483 .function_return_types
1484 .get(&(node_id, member_name.to_string()))
1485 .cloned()
1486 }
1487 AccessKind::Index => {
1488 if let Some(members) = cache.node_members.get(&node_id) {
1490 for member in members {
1491 if member.label == member_name {
1492 if let Some(tid) = cache.name_to_type.get(member_name) {
1494 if tid.starts_with("t_mapping") {
1495 return extract_mapping_value_type(tid.as_str());
1496 }
1497 return Some(tid.to_string());
1498 }
1499 }
1500 }
1501 }
1502 if let Some(tid) = cache.name_to_type.get(member_name)
1504 && tid.starts_with("t_mapping")
1505 {
1506 return extract_mapping_value_type(tid.as_str());
1507 }
1508 None
1509 }
1510 AccessKind::Plain => {
1511 cache
1513 .name_to_type
1514 .get(member_name)
1515 .map(|tid| tid.to_string())
1516 }
1517 }
1518}
1519
1520pub struct ScopeContext {
1524 pub byte_pos: usize,
1526 pub file_id: FileId,
1528}
1529
1530fn resolve_name(
1534 cache: &CompletionCache,
1535 name: &str,
1536 scope_ctx: Option<&ScopeContext>,
1537) -> Option<String> {
1538 if let Some(ctx) = scope_ctx {
1539 resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1540 } else {
1541 resolve_name_to_type_id(cache, name)
1542 }
1543}
1544
1545pub fn get_dot_completions(
1547 cache: &CompletionCache,
1548 identifier: &str,
1549 scope_ctx: Option<&ScopeContext>,
1550) -> Vec<CompletionItem> {
1551 if let Some(items) = magic_members(identifier) {
1553 return items;
1554 }
1555
1556 let type_id = resolve_name(cache, identifier, scope_ctx);
1558
1559 if let Some(tid) = type_id {
1560 return completions_for_type(cache, &tid);
1561 }
1562
1563 vec![]
1564}
1565
1566pub fn get_chain_completions(
1569 cache: &CompletionCache,
1570 chain: &[DotSegment],
1571 scope_ctx: Option<&ScopeContext>,
1572) -> Vec<CompletionItem> {
1573 if chain.is_empty() {
1574 return vec![];
1575 }
1576
1577 if chain.len() == 1 {
1579 let seg = &chain[0];
1580
1581 match seg.kind {
1583 AccessKind::Plain => {
1584 return get_dot_completions(cache, &seg.name, scope_ctx);
1585 }
1586 AccessKind::Call => {
1587 if seg.name == "type" {
1589 return type_meta_members(seg.call_args.as_deref(), Some(cache));
1590 }
1591 if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1594 return completions_for_type(cache, &type_id);
1595 }
1596 for ((_, fn_name), ret_type) in &cache.function_return_types {
1598 if fn_name == &seg.name {
1599 return completions_for_type(cache, ret_type);
1600 }
1601 }
1602 return vec![];
1603 }
1604 AccessKind::Index => {
1605 if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1607 && tid.starts_with("t_mapping")
1608 && let Some(val_type) = extract_mapping_value_type(&tid)
1609 {
1610 return completions_for_type(cache, &val_type);
1611 }
1612 return vec![];
1613 }
1614 }
1615 }
1616
1617 let first = &chain[0];
1620 let mut current_type = match first.kind {
1621 AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1622 AccessKind::Call => {
1623 resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1625 cache
1626 .function_return_types
1627 .iter()
1628 .find(|((_, fn_name), _)| fn_name == &first.name)
1629 .map(|(_, ret_type)| ret_type.clone())
1630 })
1631 }
1632 AccessKind::Index => {
1633 resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1635 if tid.starts_with("t_mapping") {
1636 extract_mapping_value_type(&tid)
1637 } else {
1638 Some(tid)
1639 }
1640 })
1641 }
1642 };
1643
1644 for seg in &chain[1..] {
1646 let ctx_type = match ¤t_type {
1647 Some(t) => t.clone(),
1648 None => return vec![],
1649 };
1650
1651 current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1652 }
1653
1654 match current_type {
1656 Some(tid) => completions_for_type(cache, &tid),
1657 None => vec![],
1658 }
1659}
1660
1661pub fn get_static_completions() -> Vec<CompletionItem> {
1664 let mut items = Vec::new();
1665
1666 for kw in SOLIDITY_KEYWORDS {
1668 items.push(CompletionItem {
1669 label: kw.to_string(),
1670 kind: Some(CompletionItemKind::KEYWORD),
1671 ..Default::default()
1672 });
1673 }
1674
1675 for (name, detail) in MAGIC_GLOBALS {
1677 items.push(CompletionItem {
1678 label: name.to_string(),
1679 kind: Some(CompletionItemKind::VARIABLE),
1680 detail: Some(detail.to_string()),
1681 ..Default::default()
1682 });
1683 }
1684
1685 for (name, detail) in GLOBAL_FUNCTIONS {
1687 items.push(CompletionItem {
1688 label: name.to_string(),
1689 kind: Some(CompletionItemKind::FUNCTION),
1690 detail: Some(detail.to_string()),
1691 ..Default::default()
1692 });
1693 }
1694
1695 for (name, detail) in ETHER_UNITS {
1697 items.push(CompletionItem {
1698 label: name.to_string(),
1699 kind: Some(CompletionItemKind::UNIT),
1700 detail: Some(detail.to_string()),
1701 ..Default::default()
1702 });
1703 }
1704
1705 for (name, detail) in TIME_UNITS {
1707 items.push(CompletionItem {
1708 label: name.to_string(),
1709 kind: Some(CompletionItemKind::UNIT),
1710 detail: Some(detail.to_string()),
1711 ..Default::default()
1712 });
1713 }
1714
1715 items
1716}
1717
1718pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1720 let mut items = cache.names.clone();
1721 items.extend(get_static_completions());
1722 items
1723}
1724
1725fn completion_item(
1726 label: impl Into<String>,
1727 kind: CompletionItemKind,
1728 detail: impl Into<String>,
1729) -> CompletionItem {
1730 CompletionItem {
1731 label: label.into(),
1732 kind: Some(kind),
1733 detail: Some(detail.into()),
1734 ..Default::default()
1735 }
1736}
1737
1738fn line_replacement_item(
1739 label: impl Into<String>,
1740 detail: impl Into<String>,
1741 new_text: impl Into<String>,
1742 position: Position,
1743) -> CompletionItem {
1744 let label = label.into();
1745 let new_text = new_text.into();
1746 CompletionItem {
1747 filter_text: Some(format!("spdx {label} {new_text}")),
1748 label,
1749 kind: Some(CompletionItemKind::CONSTANT),
1750 detail: Some(detail.into()),
1751 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1752 range: Range {
1753 start: Position {
1754 line: position.line,
1755 character: 0,
1756 },
1757 end: position,
1758 },
1759 new_text,
1760 })),
1761 ..Default::default()
1762 }
1763}
1764
1765fn prefix_at_byte(line: &str, col_byte: u32) -> &str {
1766 let mut end = (col_byte as usize).min(line.len());
1767 while !line.is_char_boundary(end) {
1768 end -= 1;
1769 }
1770 &line[..end]
1771}
1772
1773fn source_unit_directive_completions(
1774 line: &str,
1775 line_prefix: &str,
1776 position: Position,
1777) -> Option<Vec<CompletionItem>> {
1778 let trimmed = line_prefix.trim_start();
1779
1780 if let Some(after_slashes) = trimmed.strip_prefix("//") {
1781 let body = after_slashes.trim_start();
1782 let upper = body.to_ascii_uppercase();
1783
1784 if upper.starts_with("SPDX-LICENSE-IDENTIFIER:") {
1785 return Some(spdx_license_identifier_completions());
1786 }
1787
1788 if is_spdx_directive_prefix(&upper) {
1789 return Some(spdx_directive_completions(position));
1790 }
1791 }
1792
1793 if let Some(after_solidity) = strip_directive_value(trimmed, "pragma solidity") {
1794 if after_solidity.contains(';') {
1795 return Some(vec![]);
1796 }
1797
1798 return Some(solidity_version_completions(
1799 position,
1800 directive_value_range(line, "pragma solidity"),
1801 ));
1802 }
1803
1804 if let Some(after_abicoder) = strip_directive_value(trimmed, "pragma abicoder") {
1805 if after_abicoder.contains(';') {
1806 return Some(vec![]);
1807 }
1808
1809 return Some(vec![
1810 completion_item("v2", CompletionItemKind::CONSTANT, "ABI coder v2"),
1811 completion_item("v1", CompletionItemKind::CONSTANT, "ABI coder v1"),
1812 ]);
1813 }
1814
1815 if let Some(after_pragma) = trimmed.strip_prefix("pragma")
1816 && !after_pragma.is_empty()
1817 && after_pragma.trim().is_empty()
1818 {
1819 return Some(vec![
1820 completion_item(
1821 "solidity",
1822 CompletionItemKind::KEYWORD,
1823 "Compiler version pragma",
1824 ),
1825 completion_item("abicoder", CompletionItemKind::KEYWORD, "ABI coder pragma"),
1826 ]);
1827 }
1828
1829 None
1830}
1831
1832fn strip_directive_value<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
1833 let rest = line.strip_prefix(directive)?;
1834 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
1835 Some(rest)
1836 } else {
1837 None
1838 }
1839}
1840
1841fn is_spdx_directive_prefix(upper_comment_body: &str) -> bool {
1842 let body = upper_comment_body.trim_start();
1843 !body.is_empty() && ("SPDX".starts_with(body) || body.starts_with("SPDX"))
1844}
1845
1846fn spdx_directive_completions(position: Position) -> Vec<CompletionItem> {
1847 vec![
1848 line_replacement_item(
1849 "// SPDX-License-Identifier: MIT",
1850 "SPDX license identifier",
1851 "// SPDX-License-Identifier: MIT",
1852 position,
1853 ),
1854 line_replacement_item(
1855 "// SPDX-License-Identifier: UNLICENSED",
1856 "SPDX license identifier",
1857 "// SPDX-License-Identifier: UNLICENSED",
1858 position,
1859 ),
1860 line_replacement_item(
1861 "// SPDX-License-Identifier: Apache-2.0",
1862 "SPDX license identifier",
1863 "// SPDX-License-Identifier: Apache-2.0",
1864 position,
1865 ),
1866 ]
1867}
1868
1869fn spdx_license_identifier_completions() -> Vec<CompletionItem> {
1870 [
1871 "MIT",
1872 "UNLICENSED",
1873 "Apache-2.0",
1874 "GPL-3.0-only",
1875 "GPL-3.0-or-later",
1876 "BSD-3-Clause",
1877 ]
1878 .into_iter()
1879 .map(|license| completion_item(license, CompletionItemKind::CONSTANT, "SPDX license"))
1880 .collect()
1881}
1882
1883fn directive_value_range(line: &str, directive: &str) -> Range {
1884 let mut byte = line
1885 .find(directive)
1886 .map(|idx| idx + directive.len())
1887 .unwrap_or(line.len());
1888
1889 while byte < line.len() {
1890 let ch = line[byte..]
1891 .chars()
1892 .next()
1893 .expect("byte is within string bounds");
1894 if !ch.is_whitespace() {
1895 break;
1896 }
1897 byte += ch.len_utf8();
1898 }
1899
1900 let end_byte = line[byte..]
1901 .find(';')
1902 .map(|idx| byte + idx)
1903 .unwrap_or(line.len());
1904
1905 Range {
1906 start: Position {
1907 line: 0,
1908 character: line[..byte].encode_utf16().count() as u32,
1909 },
1910 end: Position {
1911 line: 0,
1912 character: line[..end_byte].encode_utf16().count() as u32,
1913 },
1914 }
1915}
1916
1917fn version_completion_item(
1918 label: impl Into<String>,
1919 detail: impl Into<String>,
1920 position: Position,
1921 range: Range,
1922) -> CompletionItem {
1923 let label = label.into();
1924 CompletionItem {
1925 label: label.clone(),
1926 kind: Some(CompletionItemKind::CONSTANT),
1927 detail: Some(detail.into()),
1928 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1929 range: Range {
1930 start: Position {
1931 line: position.line,
1932 character: range.start.character,
1933 },
1934 end: Position {
1935 line: position.line,
1936 character: range.end.character,
1937 },
1938 },
1939 new_text: label,
1940 })),
1941 ..Default::default()
1942 }
1943}
1944
1945fn solidity_version_completions(position: Position, range: Range) -> Vec<CompletionItem> {
1946 [
1947 ("^0.8.35", "Latest Solidity 0.8.x compatible range"),
1948 ("0.8.35", "Latest Solidity exact version"),
1949 ("^0.8.0", "Solidity 0.8.x compatible range"),
1950 (">=0.8.0 <0.9.0", "Solidity 0.8.x range"),
1951 ]
1952 .into_iter()
1953 .map(|(version, detail)| version_completion_item(version, detail, position, range))
1954 .collect()
1955}
1956
1957pub fn append_auto_import_candidates_last(
1963 mut base: Vec<CompletionItem>,
1964 mut auto_import_candidates: Vec<CompletionItem>,
1965) -> Vec<CompletionItem> {
1966 let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1967 for item in &auto_import_candidates {
1968 let entry = unique_label_edits
1969 .entry(item.label.clone())
1970 .or_insert_with(|| item.additional_text_edits.clone());
1971 if *entry != item.additional_text_edits {
1972 *entry = None;
1973 }
1974 }
1975
1976 for item in &mut base {
1980 if item.additional_text_edits.is_none()
1981 && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1982 {
1983 item.additional_text_edits = Some(edits.clone());
1984 }
1985 }
1986
1987 for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1988 if item.sort_text.is_none() {
1989 item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1990 }
1991 }
1992
1993 base.extend(auto_import_candidates);
1994 base
1995}
1996
1997pub fn top_level_importable_completion_candidates(
2002 cache: &CompletionCache,
2003 current_file_path: Option<&str>,
2004 source_text: &str,
2005) -> Vec<CompletionItem> {
2006 let mut out = Vec::new();
2007 for symbols in cache.top_level_importables_by_name.values() {
2008 for symbol in symbols {
2009 if let Some(cur) = current_file_path
2010 && cur == symbol.declaring_path
2011 {
2012 continue;
2013 }
2014
2015 let import_path = match current_file_path.and_then(|cur| {
2016 to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
2017 }) {
2018 Some(p) => p,
2019 None => continue,
2020 };
2021
2022 if import_statement_already_present(source_text, &symbol.name, &import_path) {
2023 continue;
2024 }
2025
2026 let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
2027 out.push(CompletionItem {
2028 label: symbol.name.clone(),
2029 kind: Some(symbol.kind),
2030 detail: Some(format!("{} ({import_path})", symbol.node_type)),
2031 additional_text_edits: import_edit.map(|e| vec![e]),
2032 ..Default::default()
2033 });
2034 }
2035 }
2036 out
2037}
2038
2039fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
2040 let from_dir = current_file.parent()?;
2041 let rel = pathdiff::diff_paths(target_file, from_dir)?;
2042 let mut s = rel.to_string_lossy().replace('\\', "/");
2043 if !s.starts_with("./") && !s.starts_with("../") {
2044 s = format!("./{s}");
2045 }
2046 Some(s)
2047}
2048
2049fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
2050 let named = format!("import {{{symbol}}} from \"{import_path}\";");
2051 let full = format!("import \"{import_path}\";");
2052 source_text.contains(&named) || source_text.contains(&full)
2053}
2054
2055fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
2056 let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
2057 let lines: Vec<&str> = source_text.lines().collect();
2058
2059 let last_import_line = lines
2060 .iter()
2061 .enumerate()
2062 .filter(|(_, line)| line.trim_start().starts_with("import "))
2063 .map(|(idx, _)| idx)
2064 .next_back();
2065
2066 let insert_line = if let Some(idx) = last_import_line {
2067 idx + 1
2068 } else if let Some(idx) = lines
2069 .iter()
2070 .enumerate()
2071 .filter(|(_, line)| line.trim_start().starts_with("pragma "))
2072 .map(|(idx, _)| idx)
2073 .next_back()
2074 {
2075 idx + 1
2076 } else {
2077 0
2078 };
2079
2080 Some(TextEdit {
2081 range: Range {
2082 start: Position {
2083 line: insert_line as u32,
2084 character: 0,
2085 },
2086 end: Position {
2087 line: insert_line as u32,
2088 character: 0,
2089 },
2090 },
2091 new_text: import_stmt,
2092 })
2093}
2094
2095pub fn handle_completion_with_tail_candidates(
2100 cache: Option<&CompletionCache>,
2101 source_text: &str,
2102 position: Position,
2103 trigger_char: Option<&str>,
2104 file_id: Option<FileId>,
2105 tail_candidates: Vec<CompletionItem>,
2106) -> Option<CompletionResponse> {
2107 let lines: Vec<&str> = source_text.lines().collect();
2108 let line = lines.get(position.line as usize)?;
2109
2110 let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
2112 let line_start_byte: usize = source_text[..abs_byte]
2113 .rfind('\n')
2114 .map(|i| i + 1)
2115 .unwrap_or(0);
2116 let col_byte = (abs_byte - line_start_byte) as u32;
2117
2118 if let Some(items) =
2119 source_unit_directive_completions(line, prefix_at_byte(line, col_byte), position)
2120 {
2121 return Some(CompletionResponse::List(CompletionList {
2122 is_incomplete: false,
2123 items,
2124 }));
2125 }
2126
2127 let scope_ctx = file_id.map(|fid| ScopeContext {
2129 byte_pos: abs_byte,
2130 file_id: fid,
2131 });
2132
2133 let items = if trigger_char == Some(".") {
2134 let chain = parse_dot_chain(line, col_byte);
2135 if chain.is_empty() {
2136 return None;
2137 }
2138 match cache {
2139 Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
2140 None => {
2141 if chain.len() == 1 {
2143 let seg = &chain[0];
2144 if seg.name == "type" && seg.kind == AccessKind::Call {
2145 type_meta_members(seg.call_args.as_deref(), None)
2147 } else if seg.kind == AccessKind::Plain {
2148 magic_members(&seg.name).unwrap_or_default()
2149 } else {
2150 vec![]
2151 }
2152 } else {
2153 vec![]
2154 }
2155 }
2156 }
2157 } else {
2158 match cache {
2159 Some(c) => {
2160 append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
2161 }
2162 None => get_static_completions(),
2163 }
2164 };
2165
2166 Some(CompletionResponse::List(CompletionList {
2167 is_incomplete: cache.is_none(),
2168 items,
2169 }))
2170}
2171
2172pub fn handle_completion(
2182 cache: Option<&CompletionCache>,
2183 source_text: &str,
2184 position: Position,
2185 trigger_char: Option<&str>,
2186 file_id: Option<FileId>,
2187) -> Option<CompletionResponse> {
2188 handle_completion_with_tail_candidates(
2189 cache,
2190 source_text,
2191 position,
2192 trigger_char,
2193 file_id,
2194 vec![],
2195 )
2196}
2197
2198const SOLIDITY_KEYWORDS: &[&str] = &[
2199 "abstract",
2200 "address",
2201 "assembly",
2202 "bool",
2203 "break",
2204 "bytes",
2205 "bytes1",
2206 "bytes4",
2207 "bytes32",
2208 "calldata",
2209 "constant",
2210 "constructor",
2211 "continue",
2212 "contract",
2213 "delete",
2214 "do",
2215 "else",
2216 "emit",
2217 "enum",
2218 "error",
2219 "event",
2220 "external",
2221 "fallback",
2222 "false",
2223 "for",
2224 "function",
2225 "if",
2226 "immutable",
2227 "import",
2228 "indexed",
2229 "int8",
2230 "int24",
2231 "int128",
2232 "int256",
2233 "interface",
2234 "internal",
2235 "library",
2236 "mapping",
2237 "memory",
2238 "modifier",
2239 "new",
2240 "override",
2241 "payable",
2242 "pragma",
2243 "private",
2244 "public",
2245 "pure",
2246 "receive",
2247 "return",
2248 "returns",
2249 "revert",
2250 "storage",
2251 "string",
2252 "struct",
2253 "true",
2254 "type",
2255 "uint8",
2256 "uint24",
2257 "uint128",
2258 "uint160",
2259 "uint256",
2260 "unchecked",
2261 "using",
2262 "view",
2263 "virtual",
2264 "while",
2265];
2266
2267const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2269
2270const TIME_UNITS: &[(&str, &str)] = &[
2272 ("seconds", "1"),
2273 ("minutes", "60 seconds"),
2274 ("hours", "3600 seconds"),
2275 ("days", "86400 seconds"),
2276 ("weeks", "604800 seconds"),
2277];
2278
2279const MAGIC_GLOBALS: &[(&str, &str)] = &[
2280 ("msg", "msg"),
2281 ("block", "block"),
2282 ("tx", "tx"),
2283 ("abi", "abi"),
2284 ("this", "address"),
2285 ("super", "contract"),
2286 ("type", "type information"),
2287];
2288
2289const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2290 ("addmod(uint256, uint256, uint256)", "uint256"),
2292 ("mulmod(uint256, uint256, uint256)", "uint256"),
2293 ("keccak256(bytes memory)", "bytes32"),
2294 ("sha256(bytes memory)", "bytes32"),
2295 ("ripemd160(bytes memory)", "bytes20"),
2296 (
2297 "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2298 "address",
2299 ),
2300 ("blockhash(uint256 blockNumber)", "bytes32"),
2302 ("blobhash(uint256 index)", "bytes32"),
2303 ("gasleft()", "uint256"),
2304 ("assert(bool condition)", ""),
2306 ("require(bool condition)", ""),
2307 ("require(bool condition, string memory message)", ""),
2308 ("revert()", ""),
2309 ("revert(string memory reason)", ""),
2310 ("selfdestruct(address payable recipient)", ""),
2312];
2313
2314pub fn all_sol_import_paths(
2330 current_file: &Path,
2331 project_root: &Path,
2332 remappings: &[String],
2333 typed_range: Option<(u32, u32, u32)>,
2334) -> Vec<CompletionItem> {
2335 let current_dir = match current_file.parent() {
2336 Some(d) => d,
2337 None => return vec![],
2338 };
2339
2340 let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2345 .iter()
2346 .filter_map(|r| {
2347 let mut it = r.splitn(2, '=');
2348 let prefix = it.next()?.to_string();
2349 let target = it.next()?;
2350 if prefix.is_empty() || target.is_empty() {
2351 return None;
2352 }
2353 let raw = project_root.join(target);
2354 let canonical = raw.canonicalize().unwrap_or(raw);
2355 Some((prefix, canonical))
2356 })
2357 .collect();
2358
2359 let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2360 let mut items = Vec::new();
2361
2362 collect_sol_files(
2363 project_root,
2364 current_dir,
2365 &parsed_remappings,
2366 skip_dirs,
2367 typed_range,
2368 &mut items,
2369 );
2370 items.sort_by(|a, b| a.label.cmp(&b.label));
2371 items
2372}
2373
2374fn collect_sol_files(
2375 dir: &Path,
2376 current_dir: &Path,
2377 remappings: &[(String, std::path::PathBuf)],
2378 skip_dirs: &[&str],
2379 typed_range: Option<(u32, u32, u32)>,
2380 out: &mut Vec<CompletionItem>,
2381) {
2382 let entries = match std::fs::read_dir(dir) {
2383 Ok(e) => e,
2384 Err(_) => return,
2385 };
2386 for entry in entries.flatten() {
2387 let path = entry.path();
2388 let name = entry.file_name();
2389 let name_str = name.to_string_lossy();
2390
2391 if name_str.starts_with('.') {
2392 continue;
2393 }
2394
2395 if path.is_dir() {
2396 if skip_dirs.contains(&name_str.as_ref()) {
2397 continue;
2398 }
2399 collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2400 continue;
2401 }
2402
2403 if !path.is_file() || !name_str.ends_with(".sol") {
2404 continue;
2405 }
2406
2407 if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2409 let s = rel.to_string_lossy().to_string();
2410 let label = if s.starts_with("../") || s.starts_with("./") {
2411 s
2412 } else {
2413 format!("./{s}")
2414 };
2415 out.push(make_import_item(label, typed_range));
2416 }
2417
2418 for (prefix, target_abs) in remappings {
2424 if let Ok(suffix) = path.strip_prefix(target_abs) {
2425 let suffix_str = suffix
2426 .to_string_lossy()
2427 .trim_start_matches(['/', '\\'])
2428 .to_string();
2429 let label = format!("{}{}", prefix, suffix_str);
2430 out.push(make_import_item(label, typed_range));
2431 }
2432 }
2433 }
2434}
2435
2436fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2437 let text_edit = typed_range.map(|(line, start_col, end_col)| {
2444 tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2445 range: Range {
2446 start: Position {
2447 line,
2448 character: start_col,
2449 },
2450 end: Position {
2451 line,
2452 character: end_col,
2453 },
2454 },
2455 new_text: label.clone(),
2456 })
2457 });
2458 CompletionItem {
2459 label: label.clone(),
2460 filter_text: Some(label.clone()),
2461 text_edit,
2462 kind: Some(CompletionItemKind::FILE),
2463 ..Default::default()
2464 }
2465}
2466
2467#[cfg(test)]
2468mod tests {
2469 use super::{
2470 CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2471 build_completion_cache, extract_top_level_importables_for_file,
2472 };
2473 use crate::types::SymbolName;
2474 use serde_json::json;
2475 use std::collections::HashMap;
2476 use tower_lsp::lsp_types::CompletionItemKind;
2477 use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2478
2479 fn empty_cache() -> CompletionCache {
2480 CompletionCache {
2481 names: vec![],
2482 name_to_type: HashMap::new(),
2483 node_members: HashMap::new(),
2484 type_to_node: HashMap::new(),
2485 name_to_node_id: HashMap::new(),
2486 method_identifiers: HashMap::new(),
2487 function_return_types: HashMap::new(),
2488 using_for: HashMap::new(),
2489 using_for_wildcard: vec![],
2490 general_completions: vec![],
2491 scope_declarations: HashMap::new(),
2492 scope_parent: HashMap::new(),
2493 scope_ranges: vec![],
2494 path_to_file_id: HashMap::new(),
2495 linearized_base_contracts: HashMap::new(),
2496 contract_kinds: HashMap::new(),
2497 top_level_importables_by_name: HashMap::new(),
2498 top_level_importables_by_file: HashMap::new(),
2499 }
2500 }
2501
2502 #[test]
2503 fn top_level_importables_include_only_direct_declared_symbols() {
2504 let sources = json!({
2505 "/tmp/A.sol": {
2506 "id": 0,
2507 "ast": {
2508 "id": 1,
2509 "nodeType": "SourceUnit",
2510 "src": "0:100:0",
2511 "nodes": [
2512 { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2513 { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2514 { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2515 ] },
2516 { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2517 { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2518 { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2519 { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2520 { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2521 { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2522 ]
2523 }
2524 }
2525 });
2526
2527 let cache = build_completion_cache(&sources, None, None);
2528 let map = &cache.top_level_importables_by_name;
2529 let by_file = &cache.top_level_importables_by_file;
2530
2531 assert!(map.contains_key("C"));
2532 assert!(map.contains_key("S"));
2533 assert!(map.contains_key("E"));
2534 assert!(map.contains_key("Wad"));
2535 assert!(map.contains_key("freeFn"));
2536 assert!(map.contains_key("TOP_CONST"));
2537
2538 assert!(!map.contains_key("Alias"));
2539 assert!(!map.contains_key("inside"));
2540 assert!(!map.contains_key("TOP_VAR"));
2541
2542 let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2543 let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2544 assert!(file_names.contains(&"C"));
2545 assert!(file_names.contains(&"TOP_CONST"));
2546 assert!(!file_names.contains(&"Alias"));
2547 }
2548
2549 #[test]
2550 fn top_level_importables_keep_multiple_declarations_for_same_name() {
2551 let sources = json!({
2552 "/tmp/A.sol": {
2553 "id": 0,
2554 "ast": {
2555 "id": 1,
2556 "nodeType": "SourceUnit",
2557 "src": "0:100:0",
2558 "nodes": [
2559 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2560 ]
2561 }
2562 },
2563 "/tmp/B.sol": {
2564 "id": 1,
2565 "ast": {
2566 "id": 2,
2567 "nodeType": "SourceUnit",
2568 "src": "0:100:1",
2569 "nodes": [
2570 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2571 ]
2572 }
2573 }
2574 });
2575
2576 let cache = build_completion_cache(&sources, None, None);
2577 let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2578 assert_eq!(entries.len(), 2);
2579 }
2580
2581 #[test]
2582 fn extract_top_level_importables_for_file_finds_expected_symbols() {
2583 let ast = json!({
2584 "id": 1,
2585 "nodeType": "SourceUnit",
2586 "src": "0:100:0",
2587 "nodes": [
2588 { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2589 { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2590 { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2591 ]
2592 });
2593
2594 let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2595 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2596 assert!(names.contains(&"f"));
2597 assert!(names.contains(&"K"));
2598 assert!(!names.contains(&"V"));
2599 }
2600
2601 #[test]
2602 fn top_level_importables_can_be_replaced_per_file() {
2603 let sources = json!({
2604 "/tmp/A.sol": {
2605 "id": 0,
2606 "ast": {
2607 "id": 1,
2608 "nodeType": "SourceUnit",
2609 "src": "0:100:0",
2610 "nodes": [
2611 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2612 ]
2613 }
2614 },
2615 "/tmp/B.sol": {
2616 "id": 1,
2617 "ast": {
2618 "id": 2,
2619 "nodeType": "SourceUnit",
2620 "src": "0:100:1",
2621 "nodes": [
2622 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2623 ]
2624 }
2625 }
2626 });
2627
2628 let mut cache = build_completion_cache(&sources, None, None);
2629 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2630
2631 cache.replace_top_level_importables_for_path(
2632 "/tmp/A.sol".to_string(),
2633 vec![TopLevelImportable {
2634 name: "newA".to_string(),
2635 declaring_path: "/tmp/A.sol".to_string(),
2636 node_type: "FunctionDefinition".to_string(),
2637 kind: CompletionItemKind::FUNCTION,
2638 }],
2639 );
2640 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2641 assert!(cache.top_level_importables_by_name.contains_key("newA"));
2642
2643 cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2644 assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2645 }
2646
2647 #[test]
2648 fn append_auto_import_candidates_last_sets_tail_sort_text() {
2649 let base = vec![CompletionItem {
2650 label: "localVar".to_string(),
2651 ..Default::default()
2652 }];
2653 let auto = vec![CompletionItem {
2654 label: "ImportMe".to_string(),
2655 ..Default::default()
2656 }];
2657
2658 let out = append_auto_import_candidates_last(base, auto);
2659 assert_eq!(out.len(), 2);
2660 assert_eq!(out[1].label, "ImportMe");
2661 assert!(
2662 out[1]
2663 .sort_text
2664 .as_deref()
2665 .is_some_and(|s| s.starts_with("zz_autoimport_"))
2666 );
2667 }
2668
2669 #[test]
2670 fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2671 let base = vec![CompletionItem {
2672 label: "B".to_string(),
2673 ..Default::default()
2674 }];
2675 let auto = vec![
2676 CompletionItem {
2677 label: "B".to_string(),
2678 detail: Some("ContractDefinition (./B.sol)".to_string()),
2679 ..Default::default()
2680 },
2681 CompletionItem {
2682 label: "B".to_string(),
2683 detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2684 ..Default::default()
2685 },
2686 ];
2687
2688 let out = append_auto_import_candidates_last(base, auto);
2689 assert_eq!(out.len(), 3);
2690 }
2691
2692 #[test]
2693 fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2694 let base = vec![CompletionItem {
2695 label: "B".to_string(),
2696 ..Default::default()
2697 }];
2698 let auto = vec![CompletionItem {
2699 label: "B".to_string(),
2700 additional_text_edits: Some(vec![TextEdit {
2701 range: Range {
2702 start: Position {
2703 line: 0,
2704 character: 0,
2705 },
2706 end: Position {
2707 line: 0,
2708 character: 0,
2709 },
2710 },
2711 new_text: "import {B} from \"./B.sol\";\n".to_string(),
2712 }]),
2713 ..Default::default()
2714 }];
2715 let out = append_auto_import_candidates_last(base, auto);
2716 assert!(
2717 out[0].additional_text_edits.is_some(),
2718 "base item should inherit unique import edit"
2719 );
2720 }
2721
2722 #[test]
2723 fn top_level_importable_candidates_include_import_edit() {
2724 let mut cache = empty_cache();
2725 cache.top_level_importables_by_name.insert(
2726 SymbolName::new("B"),
2727 vec![TopLevelImportable {
2728 name: "B".to_string(),
2729 declaring_path: "/tmp/example/B.sol".to_string(),
2730 node_type: "ContractDefinition".to_string(),
2731 kind: CompletionItemKind::CLASS,
2732 }],
2733 );
2734
2735 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2736 let items = super::top_level_importable_completion_candidates(
2737 &cache,
2738 Some("/tmp/example/A.sol"),
2739 source,
2740 );
2741 assert_eq!(items.len(), 1);
2742 let edit_text = items[0]
2743 .additional_text_edits
2744 .as_ref()
2745 .and_then(|edits| edits.first())
2746 .map(|e| e.new_text.clone())
2747 .unwrap_or_default();
2748 assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2749 }
2750
2751 #[test]
2752 fn handle_completion_general_path_keeps_base_items() {
2753 let mut cache = empty_cache();
2754 cache.general_completions = vec![CompletionItem {
2755 label: "A".to_string(),
2756 ..Default::default()
2757 }];
2758
2759 let resp = super::handle_completion(
2760 Some(&cache),
2761 "contract X {}",
2762 Position {
2763 line: 0,
2764 character: 0,
2765 },
2766 None,
2767 None,
2768 );
2769 match resp {
2770 Some(CompletionResponse::List(list)) => {
2771 assert_eq!(list.items.len(), 1);
2772 assert_eq!(list.items[0].label, "A");
2773 }
2774 _ => panic!("expected completion list"),
2775 }
2776 }
2777
2778 #[test]
2781 fn make_import_item_has_filter_text_and_text_edit() {
2782 let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
2783 assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
2784 assert!(item.text_edit.is_some(), "text_edit should be set");
2785 assert!(
2786 item.insert_text.is_none(),
2787 "insert_text should be absent when text_edit is set"
2788 );
2789 }
2790
2791 #[test]
2792 fn all_sol_import_paths_no_panic_missing_dir() {
2793 let current = std::path::Path::new("/nonexistent/src/Foo.sol");
2794 let root = std::path::Path::new("/nonexistent");
2795 let items = super::all_sol_import_paths(current, root, &[], None);
2796 assert!(items.is_empty());
2797 }
2798
2799 #[test]
2800 fn all_sol_import_paths_relative_labels() {
2801 let root = std::path::Path::new("example");
2803 let current = root.join("A.sol");
2804 let items = super::all_sol_import_paths(¤t, root, &[], None);
2805 for item in &items {
2807 assert!(
2808 item.label.starts_with("./") || item.label.starts_with("../"),
2809 "label should be relative: {}",
2810 item.label
2811 );
2812 assert!(
2813 item.label.ends_with(".sol"),
2814 "label should end with .sol: {}",
2815 item.label
2816 );
2817 }
2818 assert!(!items.is_empty(), "should find at least one .sol file");
2819 }
2820}