1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use tower_lsp::lsp_types::{
5 CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, Position, Range,
6 TextEdit,
7};
8
9use crate::goto::CHILD_KEYS;
10use crate::hover::build_function_signature;
11use crate::types::{FileId, NodeId, SourceLoc};
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<String, String>,
57
58 pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
60
61 pub type_to_node: HashMap<String, NodeId>,
63
64 pub name_to_node_id: HashMap<String, 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<String, 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<String, 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<String, Vec<TopLevelImportable>>,
119
120 pub top_level_importables_by_file: HashMap<String, 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::<u64>()
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<String, Vec<TopLevelImportable>>,
295) -> HashMap<String, Vec<TopLevelImportable>> {
296 let mut by_name: HashMap<String, Vec<TopLevelImportable>> = HashMap::new();
297 for symbols in by_file.values() {
298 for symbol in symbols {
299 by_name
300 .entry(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_u64()).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_u64()) == 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.insert(path, symbols);
355 self.top_level_importables_by_name =
356 build_top_level_importables_by_name(&self.top_level_importables_by_file);
357 }
358}
359
360pub fn build_completion_cache(sources: &Value, contracts: Option<&Value>) -> CompletionCache {
363 let source_count = sources.as_object().map_or(0, |obj| obj.len());
364 let est_names = source_count * 20;
367 let est_contracts = source_count * 5;
368
369 let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
370 let mut seen_names: HashMap<String, usize> = HashMap::with_capacity(est_names);
371 let mut name_to_type: HashMap<String, String> = HashMap::with_capacity(est_names);
372 let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
373 HashMap::with_capacity(est_contracts);
374 let mut type_to_node: HashMap<String, NodeId> = HashMap::with_capacity(est_contracts);
375 let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
376 HashMap::with_capacity(est_contracts);
377 let mut name_to_node_id: HashMap<String, NodeId> = HashMap::with_capacity(est_names);
378 let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
379
380 let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
382
383 let mut function_signatures: HashMap<NodeId, HashMap<String, Vec<String>>> =
385 HashMap::with_capacity(est_contracts);
386
387 let mut function_return_types: HashMap<(NodeId, String), String> =
389 HashMap::with_capacity(source_count * 10);
390
391 let mut using_for: HashMap<String, Vec<CompletionItem>> = HashMap::with_capacity(source_count);
393 let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
394
395 let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
397
398 let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
400 HashMap::with_capacity(est_contracts);
401 let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
402 let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
403 let mut path_to_file_id: HashMap<String, FileId> = HashMap::with_capacity(source_count);
404 let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
405 HashMap::with_capacity(est_contracts);
406 let mut top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>> =
407 HashMap::with_capacity(est_names);
408
409 if let Some(sources_obj) = sources.as_object() {
410 for (path, source_data) in sources_obj {
411 if let Some(ast) = source_data.get("ast") {
412 if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
414 path_to_file_id.insert(path.clone(), FileId(fid));
415 }
416 let file_importables = extract_top_level_importables_for_file(path, ast);
417 if !file_importables.is_empty() {
418 top_level_importables_by_file.insert(path.clone(), file_importables);
419 }
420 let mut stack: Vec<&Value> = vec![ast];
421
422 while let Some(tree) = stack.pop() {
423 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
424 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
425 let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
426
427 let is_scope_node = matches!(
432 node_type,
433 "SourceUnit"
434 | "ContractDefinition"
435 | "FunctionDefinition"
436 | "ModifierDefinition"
437 | "Block"
438 | "UncheckedBlock"
439 );
440 if is_scope_node && let Some(nid) = node_id {
441 if let Some(src_loc) = parse_src(tree) {
442 scope_ranges.push(ScopeRange {
443 node_id: nid,
444 start: src_loc.offset,
445 end: src_loc.end(),
446 file_id: src_loc.file_id,
447 });
448 }
449 if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_u64()) {
451 scope_parent.insert(nid, NodeId(parent_id));
452 }
453 }
454
455 if node_type == "ContractDefinition"
457 && let Some(nid) = node_id
458 && let Some(bases) = tree
459 .get("linearizedBaseContracts")
460 .and_then(|v| v.as_array())
461 {
462 let base_ids: Vec<NodeId> = bases
463 .iter()
464 .filter_map(|b| b.as_u64())
465 .map(NodeId)
466 .collect();
467 if !base_ids.is_empty() {
468 linearized_base_contracts.insert(nid, base_ids);
469 }
470 }
471
472 if node_type == "VariableDeclaration"
474 && !name.is_empty()
475 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
476 && let Some(tid) = tree
477 .get("typeDescriptions")
478 .and_then(|td| td.get("typeIdentifier"))
479 .and_then(|v| v.as_str())
480 {
481 scope_declarations
482 .entry(NodeId(scope_raw))
483 .or_default()
484 .push(ScopedDeclaration {
485 name: name.to_string(),
486 type_id: tid.to_string(),
487 });
488 }
489
490 if node_type == "FunctionDefinition"
492 && !name.is_empty()
493 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
494 && let Some(tid) = tree
495 .get("typeDescriptions")
496 .and_then(|td| td.get("typeIdentifier"))
497 .and_then(|v| v.as_str())
498 {
499 scope_declarations
500 .entry(NodeId(scope_raw))
501 .or_default()
502 .push(ScopedDeclaration {
503 name: name.to_string(),
504 type_id: tid.to_string(),
505 });
506 }
507
508 if !name.is_empty() && !seen_names.contains_key(name) {
510 let type_string = tree
511 .get("typeDescriptions")
512 .and_then(|td| td.get("typeString"))
513 .and_then(|v| v.as_str())
514 .map(|s| s.to_string());
515
516 let type_id = tree
517 .get("typeDescriptions")
518 .and_then(|td| td.get("typeIdentifier"))
519 .and_then(|v| v.as_str());
520
521 let kind = node_type_to_completion_kind(node_type);
522
523 let item = CompletionItem {
524 label: name.to_string(),
525 kind: Some(kind),
526 detail: type_string,
527 ..Default::default()
528 };
529
530 let idx = names.len();
531 names.push(item);
532 seen_names.insert(name.to_string(), idx);
533
534 if let Some(tid) = type_id {
536 name_to_type.insert(name.to_string(), tid.to_string());
537 }
538 }
539
540 if node_type == "StructDefinition"
542 && let Some(id) = node_id
543 {
544 let mut members = Vec::new();
545 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
546 for member in member_array {
547 let member_name =
548 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
549 if member_name.is_empty() {
550 continue;
551 }
552 let member_type = member
553 .get("typeDescriptions")
554 .and_then(|td| td.get("typeString"))
555 .and_then(|v| v.as_str())
556 .map(|s| s.to_string());
557
558 members.push(CompletionItem {
559 label: member_name.to_string(),
560 kind: Some(CompletionItemKind::FIELD),
561 detail: member_type,
562 ..Default::default()
563 });
564 }
565 }
566 if !members.is_empty() {
567 node_members.insert(id, members);
568 }
569
570 if let Some(tid) = tree
572 .get("typeDescriptions")
573 .and_then(|td| td.get("typeIdentifier"))
574 .and_then(|v| v.as_str())
575 {
576 type_to_node.insert(tid.to_string(), id);
577 }
578 }
579
580 if node_type == "ContractDefinition"
582 && let Some(id) = node_id
583 {
584 let mut members = Vec::new();
585 let mut fn_sigs: HashMap<String, Vec<String>> = HashMap::new();
586 if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
587 for member in nodes_array {
588 let member_type = member
589 .get("nodeType")
590 .and_then(|v| v.as_str())
591 .unwrap_or("");
592 let member_name =
593 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
594 if member_name.is_empty() {
595 continue;
596 }
597
598 let (member_detail, label_details) =
600 if member_type == "FunctionDefinition" {
601 if let Some(ret_params) = member
605 .get("returnParameters")
606 .and_then(|rp| rp.get("parameters"))
607 .and_then(|v| v.as_array())
608 && ret_params.len() == 1
609 && let Some(ret_tid) = ret_params[0]
610 .get("typeDescriptions")
611 .and_then(|td| td.get("typeIdentifier"))
612 .and_then(|v| v.as_str())
613 {
614 function_return_types.insert(
615 (id, member_name.to_string()),
616 ret_tid.to_string(),
617 );
618 }
619
620 if let Some(sig) = build_function_signature(member) {
621 fn_sigs
622 .entry(member_name.to_string())
623 .or_default()
624 .push(sig.clone());
625 (Some(sig), None)
626 } else {
627 (
628 member
629 .get("typeDescriptions")
630 .and_then(|td| td.get("typeString"))
631 .and_then(|v| v.as_str())
632 .map(|s| s.to_string()),
633 None,
634 )
635 }
636 } else {
637 (
638 member
639 .get("typeDescriptions")
640 .and_then(|td| td.get("typeString"))
641 .and_then(|v| v.as_str())
642 .map(|s| s.to_string()),
643 None,
644 )
645 };
646
647 let kind = node_type_to_completion_kind(member_type);
648 members.push(CompletionItem {
649 label: member_name.to_string(),
650 kind: Some(kind),
651 detail: member_detail,
652 label_details,
653 ..Default::default()
654 });
655 }
656 }
657 if !members.is_empty() {
658 node_members.insert(id, members);
659 }
660 if !fn_sigs.is_empty() {
661 function_signatures.insert(id, fn_sigs);
662 }
663
664 if let Some(tid) = tree
665 .get("typeDescriptions")
666 .and_then(|td| td.get("typeIdentifier"))
667 .and_then(|v| v.as_str())
668 {
669 type_to_node.insert(tid.to_string(), id);
670 }
671
672 if !name.is_empty() {
674 contract_locations.push((path.clone(), name.to_string(), id));
675 name_to_node_id.insert(name.to_string(), id);
676 }
677
678 if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
680 contract_kinds.insert(id, ck.to_string());
681 }
682 }
683
684 if node_type == "EnumDefinition"
686 && let Some(id) = node_id
687 {
688 let mut members = Vec::new();
689 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
690 for member in member_array {
691 let member_name =
692 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
693 if member_name.is_empty() {
694 continue;
695 }
696 members.push(CompletionItem {
697 label: member_name.to_string(),
698 kind: Some(CompletionItemKind::ENUM_MEMBER),
699 detail: None,
700 ..Default::default()
701 });
702 }
703 }
704 if !members.is_empty() {
705 node_members.insert(id, members);
706 }
707
708 if let Some(tid) = tree
709 .get("typeDescriptions")
710 .and_then(|td| td.get("typeIdentifier"))
711 .and_then(|v| v.as_str())
712 {
713 type_to_node.insert(tid.to_string(), id);
714 }
715 }
716
717 if node_type == "UsingForDirective" {
719 let target_type = tree.get("typeName").and_then(|tn| {
721 tn.get("typeDescriptions")
722 .and_then(|td| td.get("typeIdentifier"))
723 .and_then(|v| v.as_str())
724 .map(|s| s.to_string())
725 });
726
727 if let Some(lib) = tree.get("libraryName") {
729 if let Some(lib_id) =
730 lib.get("referencedDeclaration").and_then(|v| v.as_u64())
731 {
732 using_for_directives.push((NodeId(lib_id), target_type));
733 }
734 }
735 else if let Some(func_list) =
739 tree.get("functionList").and_then(|v| v.as_array())
740 {
741 for entry in func_list {
742 if entry.get("operator").is_some() {
744 continue;
745 }
746 if let Some(def) = entry.get("definition") {
747 let fn_name =
748 def.get("name").and_then(|v| v.as_str()).unwrap_or("");
749 if !fn_name.is_empty() {
750 let items = if let Some(ref tid) = target_type {
751 using_for.entry(tid.clone()).or_default()
752 } else {
753 &mut using_for_wildcard
754 };
755 items.push(CompletionItem {
756 label: fn_name.to_string(),
757 kind: Some(CompletionItemKind::FUNCTION),
758 detail: None,
759 ..Default::default()
760 });
761 }
762 }
763 }
764 }
765 }
766
767 for key in CHILD_KEYS {
769 push_if_node_or_array(tree, key, &mut stack);
770 }
771 }
772 }
773 }
774 }
775
776 for (lib_id, target_type) in &using_for_directives {
779 if let Some(lib_members) = node_members.get(lib_id) {
780 let items: Vec<CompletionItem> = lib_members
781 .iter()
782 .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
783 .cloned()
784 .collect();
785 if !items.is_empty() {
786 if let Some(tid) = target_type {
787 using_for.entry(tid.clone()).or_default().extend(items);
788 } else {
789 using_for_wildcard.extend(items);
790 }
791 }
792 }
793 }
794
795 if let Some(contracts_val) = contracts
797 && let Some(contracts_obj) = contracts_val.as_object()
798 {
799 for (path, contract_name, node_id) in &contract_locations {
800 let fn_sigs = function_signatures.get(node_id);
802
803 if let Some(path_entry) = contracts_obj.get(path)
804 && let Some(contract_entry) = path_entry.get(contract_name)
805 && let Some(evm) = contract_entry.get("evm")
806 && let Some(methods) = evm.get("methodIdentifiers")
807 && let Some(methods_obj) = methods.as_object()
808 {
809 let mut items: Vec<CompletionItem> = Vec::new();
810 for (signature, selector_val) in methods_obj {
811 let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
814 let selector_str = selector_val
815 .as_str()
816 .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
817 .unwrap_or_default();
818
819 let description =
821 fn_sigs
822 .and_then(|sigs| sigs.get(&fn_name))
823 .and_then(|sig_list| {
824 if sig_list.len() == 1 {
825 Some(sig_list[0].clone())
827 } else {
828 let abi_param_count = count_abi_params(signature);
830 sig_list
831 .iter()
832 .find(|s| count_signature_params(s) == abi_param_count)
833 .cloned()
834 }
835 });
836
837 items.push(CompletionItem {
838 label: fn_name,
839 kind: Some(CompletionItemKind::FUNCTION),
840 detail: Some(signature.clone()),
841 label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
842 detail: Some(selector_str),
843 description,
844 }),
845 ..Default::default()
846 });
847 }
848 if !items.is_empty() {
849 method_identifiers.insert(*node_id, items);
850 }
851 }
852 }
853 }
854
855 let mut general_completions = names.clone();
857 general_completions.extend(get_static_completions());
858
859 scope_ranges.sort_by_key(|r| r.end - r.start);
861
862 let orphan_ids: Vec<NodeId> = scope_ranges
868 .iter()
869 .filter(|r| !scope_parent.contains_key(&r.node_id))
870 .map(|r| r.node_id)
871 .collect();
872 let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
874 .iter()
875 .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
876 .collect();
877 for orphan_id in &orphan_ids {
878 if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
879 let parent = scope_ranges
882 .iter()
883 .find(|r| {
884 r.node_id != *orphan_id
885 && r.file_id == file_id
886 && r.start <= start
887 && r.end >= end
888 && (r.end - r.start) > (end - start)
889 })
890 .map(|r| r.node_id);
891 if let Some(parent_id) = parent {
892 scope_parent.insert(*orphan_id, parent_id);
893 }
894 }
895 }
896
897 let top_level_importables_by_name =
898 build_top_level_importables_by_name(&top_level_importables_by_file);
899
900 CompletionCache {
901 names,
902 name_to_type,
903 node_members,
904 type_to_node,
905 name_to_node_id,
906 method_identifiers,
907 function_return_types,
908 using_for,
909 using_for_wildcard,
910 general_completions,
911 scope_declarations,
912 scope_parent,
913 scope_ranges,
914 path_to_file_id,
915 linearized_base_contracts,
916 contract_kinds,
917 top_level_importables_by_name,
918 top_level_importables_by_file,
919 }
920}
921
922fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
924 let items = match name {
925 "msg" => vec![
926 ("data", "bytes calldata"),
927 ("sender", "address"),
928 ("sig", "bytes4"),
929 ("value", "uint256"),
930 ],
931 "block" => vec![
932 ("basefee", "uint256"),
933 ("blobbasefee", "uint256"),
934 ("chainid", "uint256"),
935 ("coinbase", "address payable"),
936 ("difficulty", "uint256"),
937 ("gaslimit", "uint256"),
938 ("number", "uint256"),
939 ("prevrandao", "uint256"),
940 ("timestamp", "uint256"),
941 ],
942 "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
943 "abi" => vec![
944 ("decode(bytes memory, (...))", "..."),
945 ("encode(...)", "bytes memory"),
946 ("encodePacked(...)", "bytes memory"),
947 ("encodeWithSelector(bytes4, ...)", "bytes memory"),
948 ("encodeWithSignature(string memory, ...)", "bytes memory"),
949 ("encodeCall(function, (...))", "bytes memory"),
950 ],
951 "type" => vec![
954 ("name", "string"),
955 ("creationCode", "bytes memory"),
956 ("runtimeCode", "bytes memory"),
957 ("interfaceId", "bytes4"),
958 ("min", "T"),
959 ("max", "T"),
960 ],
961 "bytes" => vec![("concat(...)", "bytes memory")],
963 "string" => vec![("concat(...)", "string memory")],
964 _ => return None,
965 };
966
967 Some(
968 items
969 .into_iter()
970 .map(|(label, detail)| CompletionItem {
971 label: label.to_string(),
972 kind: Some(CompletionItemKind::PROPERTY),
973 detail: Some(detail.to_string()),
974 ..Default::default()
975 })
976 .collect(),
977 )
978}
979
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
983enum TypeMetaKind {
984 Contract,
986 Interface,
988 IntegerType,
990 Unknown,
992}
993
994fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
996 if arg == "int" || arg == "uint" {
998 return TypeMetaKind::IntegerType;
999 }
1000 if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1001 && let Ok(n) = suffix.parse::<u16>()
1002 && (8..=256).contains(&n)
1003 && n % 8 == 0
1004 {
1005 return TypeMetaKind::IntegerType;
1006 }
1007
1008 if let Some(c) = cache
1010 && let Some(&node_id) = c.name_to_node_id.get(arg)
1011 {
1012 return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1013 Some("interface") => TypeMetaKind::Interface,
1014 Some("library") => TypeMetaKind::Contract, _ => TypeMetaKind::Contract,
1016 };
1017 }
1018
1019 TypeMetaKind::Unknown
1020}
1021
1022fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1024 let kind = match arg {
1025 Some(a) => classify_type_arg(a, cache),
1026 None => TypeMetaKind::Unknown,
1027 };
1028
1029 let items: Vec<(&str, &str)> = match kind {
1030 TypeMetaKind::Contract => vec![
1031 ("name", "string"),
1032 ("creationCode", "bytes memory"),
1033 ("runtimeCode", "bytes memory"),
1034 ],
1035 TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1036 TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1037 TypeMetaKind::Unknown => vec![
1038 ("name", "string"),
1039 ("creationCode", "bytes memory"),
1040 ("runtimeCode", "bytes memory"),
1041 ("interfaceId", "bytes4"),
1042 ("min", "T"),
1043 ("max", "T"),
1044 ],
1045 };
1046
1047 items
1048 .into_iter()
1049 .map(|(label, detail)| CompletionItem {
1050 label: label.to_string(),
1051 kind: Some(CompletionItemKind::PROPERTY),
1052 detail: Some(detail.to_string()),
1053 ..Default::default()
1054 })
1055 .collect()
1056}
1057
1058fn address_members() -> Vec<CompletionItem> {
1060 [
1061 ("balance", "uint256", CompletionItemKind::PROPERTY),
1062 ("code", "bytes memory", CompletionItemKind::PROPERTY),
1063 ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1064 ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1065 ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1066 (
1067 "call(bytes memory)",
1068 "(bool, bytes memory)",
1069 CompletionItemKind::FUNCTION,
1070 ),
1071 (
1072 "delegatecall(bytes memory)",
1073 "(bool, bytes memory)",
1074 CompletionItemKind::FUNCTION,
1075 ),
1076 (
1077 "staticcall(bytes memory)",
1078 "(bool, bytes memory)",
1079 CompletionItemKind::FUNCTION,
1080 ),
1081 ]
1082 .iter()
1083 .map(|(label, detail, kind)| CompletionItem {
1084 label: label.to_string(),
1085 kind: Some(*kind),
1086 detail: if detail.is_empty() {
1087 None
1088 } else {
1089 Some(detail.to_string())
1090 },
1091 ..Default::default()
1092 })
1093 .collect()
1094}
1095
1096#[derive(Debug, Clone, PartialEq)]
1098pub enum AccessKind {
1099 Plain,
1101 Call,
1103 Index,
1105}
1106
1107#[derive(Debug, Clone, PartialEq)]
1109pub struct DotSegment {
1110 pub name: String,
1111 pub kind: AccessKind,
1112 pub call_args: Option<String>,
1115}
1116
1117fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1121 let close = bytes[pos];
1122 let open = match close {
1123 b')' => b'(',
1124 b']' => b'[',
1125 _ => return pos,
1126 };
1127 let mut depth = 1u32;
1128 let mut i = pos;
1129 while i > 0 && depth > 0 {
1130 i -= 1;
1131 if bytes[i] == close {
1132 depth += 1;
1133 } else if bytes[i] == open {
1134 depth -= 1;
1135 }
1136 }
1137 i
1138}
1139
1140pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1145 let col = character as usize;
1146 if col == 0 {
1147 return vec![];
1148 }
1149
1150 let bytes = line.as_bytes();
1151 let mut segments: Vec<DotSegment> = Vec::new();
1152
1153 let mut pos = col;
1155 if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1156 pos -= 1;
1157 }
1158
1159 loop {
1160 if pos == 0 {
1161 break;
1162 }
1163
1164 let (kind, call_args) = if bytes[pos - 1] == b')' {
1166 let close = pos - 1; pos = skip_brackets_backwards(bytes, close);
1168 let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1170 .trim()
1171 .to_string();
1172 let args = if args_text.is_empty() {
1173 None
1174 } else {
1175 Some(args_text)
1176 };
1177 (AccessKind::Call, args)
1178 } else if bytes[pos - 1] == b']' {
1179 pos -= 1; pos = skip_brackets_backwards(bytes, pos);
1181 (AccessKind::Index, None)
1182 } else {
1183 (AccessKind::Plain, None)
1184 };
1185
1186 let end = pos;
1188 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1189 pos -= 1;
1190 }
1191
1192 if pos == end {
1193 break;
1195 }
1196
1197 let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1198 segments.push(DotSegment {
1199 name,
1200 kind,
1201 call_args,
1202 });
1203
1204 if pos > 0 && bytes[pos - 1] == b'.' {
1206 pos -= 1; } else {
1208 break;
1209 }
1210 }
1211
1212 segments.reverse(); segments
1214}
1215
1216pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1219 let segments = parse_dot_chain(line, character);
1220 segments.last().map(|s| s.name.clone())
1221}
1222
1223#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1224Solidity AST uses different suffixes in different contexts:
1225 - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1226 - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1227 - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1228All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1229fn strip_type_suffix(type_id: &str) -> &str {
1230 let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1231 s.strip_suffix("_storage")
1232 .or_else(|| s.strip_suffix("_memory"))
1233 .or_else(|| s.strip_suffix("_calldata"))
1234 .unwrap_or(s)
1235}
1236
1237fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1241 if let Some(items) = cache.using_for.get(type_id) {
1243 return items.clone();
1244 }
1245
1246 let base = strip_type_suffix(type_id);
1248 let variants = [
1249 base.to_string(),
1250 format!("{}_storage", base),
1251 format!("{}_storage_ptr", base),
1252 format!("{}_memory", base),
1253 format!("{}_memory_ptr", base),
1254 format!("{}_calldata", base),
1255 ];
1256 for variant in &variants {
1257 if variant.as_str() != type_id
1258 && let Some(items) = cache.using_for.get(variant.as_str())
1259 {
1260 return items.clone();
1261 }
1262 }
1263
1264 vec![]
1265}
1266
1267fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1270 if type_id == "t_address" || type_id == "t_address_payable" {
1272 let mut items = address_members();
1273 if let Some(uf) = cache.using_for.get(type_id) {
1275 items.extend(uf.iter().cloned());
1276 }
1277 items.extend(cache.using_for_wildcard.iter().cloned());
1278 return items;
1279 }
1280
1281 let resolved_node_id = extract_node_id_from_type(type_id)
1282 .or_else(|| cache.type_to_node.get(type_id).copied())
1283 .or_else(|| {
1284 type_id
1286 .strip_prefix("__node_id_")
1287 .and_then(|s| s.parse::<u64>().ok())
1288 .map(NodeId)
1289 });
1290
1291 let mut items = Vec::new();
1292 let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1293
1294 if let Some(node_id) = resolved_node_id {
1295 if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1297 for item in method_items {
1298 seen_labels.insert(item.label.clone());
1299 items.push(item.clone());
1300 }
1301 }
1302
1303 if let Some(members) = cache.node_members.get(&node_id) {
1305 for item in members {
1306 if !seen_labels.contains(&item.label) {
1307 seen_labels.insert(item.label.clone());
1308 items.push(item.clone());
1309 }
1310 }
1311 }
1312 }
1313
1314 let is_contract_name = resolved_node_id
1318 .map(|nid| cache.contract_kinds.contains_key(&nid))
1319 .unwrap_or(false);
1320
1321 if !is_contract_name {
1322 let uf_items = lookup_using_for(cache, type_id);
1324 for item in &uf_items {
1325 if !seen_labels.contains(&item.label) {
1326 seen_labels.insert(item.label.clone());
1327 items.push(item.clone());
1328 }
1329 }
1330
1331 for item in &cache.using_for_wildcard {
1333 if !seen_labels.contains(&item.label) {
1334 seen_labels.insert(item.label.clone());
1335 items.push(item.clone());
1336 }
1337 }
1338 }
1339
1340 items
1341}
1342
1343fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1345 if let Some(tid) = cache.name_to_type.get(name) {
1347 return Some(tid.clone());
1348 }
1349 if let Some(node_id) = cache.name_to_node_id.get(name) {
1351 for (tid, nid) in &cache.type_to_node {
1353 if nid == node_id {
1354 return Some(tid.clone());
1355 }
1356 }
1357 return Some(format!("__node_id_{}", node_id));
1359 }
1360 None
1361}
1362
1363pub fn find_innermost_scope(
1367 cache: &CompletionCache,
1368 byte_pos: usize,
1369 file_id: FileId,
1370) -> Option<NodeId> {
1371 cache
1373 .scope_ranges
1374 .iter()
1375 .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1376 .map(|r| r.node_id)
1377}
1378
1379pub fn resolve_name_in_scope(
1388 cache: &CompletionCache,
1389 name: &str,
1390 byte_pos: usize,
1391 file_id: FileId,
1392) -> Option<String> {
1393 let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1394
1395 loop {
1397 if let Some(decls) = cache.scope_declarations.get(¤t_scope) {
1399 for decl in decls {
1400 if decl.name == name {
1401 return Some(decl.type_id.clone());
1402 }
1403 }
1404 }
1405
1406 if let Some(bases) = cache.linearized_base_contracts.get(¤t_scope) {
1410 for &base_id in bases.iter().skip(1) {
1411 if let Some(decls) = cache.scope_declarations.get(&base_id) {
1412 for decl in decls {
1413 if decl.name == name {
1414 return Some(decl.type_id.clone());
1415 }
1416 }
1417 }
1418 }
1419 }
1420
1421 match cache.scope_parent.get(¤t_scope) {
1423 Some(&parent_id) => current_scope = parent_id,
1424 None => break, }
1426 }
1427
1428 resolve_name_to_type_id(cache, name)
1431}
1432
1433fn resolve_member_type(
1438 cache: &CompletionCache,
1439 context_type_id: &str,
1440 member_name: &str,
1441 kind: &AccessKind,
1442) -> Option<String> {
1443 let resolved_node_id = extract_node_id_from_type(context_type_id)
1444 .or_else(|| cache.type_to_node.get(context_type_id).copied())
1445 .or_else(|| {
1446 context_type_id
1448 .strip_prefix("__node_id_")
1449 .and_then(|s| s.parse::<u64>().ok())
1450 .map(NodeId)
1451 });
1452
1453 let node_id = resolved_node_id?;
1454
1455 match kind {
1456 AccessKind::Call => {
1457 cache
1459 .function_return_types
1460 .get(&(node_id, member_name.to_string()))
1461 .cloned()
1462 }
1463 AccessKind::Index => {
1464 if let Some(members) = cache.node_members.get(&node_id) {
1466 for member in members {
1467 if member.label == member_name {
1468 if let Some(tid) = cache.name_to_type.get(member_name) {
1470 if tid.starts_with("t_mapping") {
1471 return extract_mapping_value_type(tid);
1472 }
1473 return Some(tid.clone());
1474 }
1475 }
1476 }
1477 }
1478 if let Some(tid) = cache.name_to_type.get(member_name)
1480 && tid.starts_with("t_mapping")
1481 {
1482 return extract_mapping_value_type(tid);
1483 }
1484 None
1485 }
1486 AccessKind::Plain => {
1487 cache.name_to_type.get(member_name).cloned()
1489 }
1490 }
1491}
1492
1493pub struct ScopeContext {
1497 pub byte_pos: usize,
1499 pub file_id: FileId,
1501}
1502
1503fn resolve_name(
1507 cache: &CompletionCache,
1508 name: &str,
1509 scope_ctx: Option<&ScopeContext>,
1510) -> Option<String> {
1511 if let Some(ctx) = scope_ctx {
1512 resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1513 } else {
1514 resolve_name_to_type_id(cache, name)
1515 }
1516}
1517
1518pub fn get_dot_completions(
1520 cache: &CompletionCache,
1521 identifier: &str,
1522 scope_ctx: Option<&ScopeContext>,
1523) -> Vec<CompletionItem> {
1524 if let Some(items) = magic_members(identifier) {
1526 return items;
1527 }
1528
1529 let type_id = resolve_name(cache, identifier, scope_ctx);
1531
1532 if let Some(tid) = type_id {
1533 return completions_for_type(cache, &tid);
1534 }
1535
1536 vec![]
1537}
1538
1539pub fn get_chain_completions(
1542 cache: &CompletionCache,
1543 chain: &[DotSegment],
1544 scope_ctx: Option<&ScopeContext>,
1545) -> Vec<CompletionItem> {
1546 if chain.is_empty() {
1547 return vec![];
1548 }
1549
1550 if chain.len() == 1 {
1552 let seg = &chain[0];
1553
1554 match seg.kind {
1556 AccessKind::Plain => {
1557 return get_dot_completions(cache, &seg.name, scope_ctx);
1558 }
1559 AccessKind::Call => {
1560 if seg.name == "type" {
1562 return type_meta_members(seg.call_args.as_deref(), Some(cache));
1563 }
1564 if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1567 return completions_for_type(cache, &type_id);
1568 }
1569 for ((_, fn_name), ret_type) in &cache.function_return_types {
1571 if fn_name == &seg.name {
1572 return completions_for_type(cache, ret_type);
1573 }
1574 }
1575 return vec![];
1576 }
1577 AccessKind::Index => {
1578 if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1580 && tid.starts_with("t_mapping")
1581 && let Some(val_type) = extract_mapping_value_type(&tid)
1582 {
1583 return completions_for_type(cache, &val_type);
1584 }
1585 return vec![];
1586 }
1587 }
1588 }
1589
1590 let first = &chain[0];
1593 let mut current_type = match first.kind {
1594 AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1595 AccessKind::Call => {
1596 resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1598 cache
1599 .function_return_types
1600 .iter()
1601 .find(|((_, fn_name), _)| fn_name == &first.name)
1602 .map(|(_, ret_type)| ret_type.clone())
1603 })
1604 }
1605 AccessKind::Index => {
1606 resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1608 if tid.starts_with("t_mapping") {
1609 extract_mapping_value_type(&tid)
1610 } else {
1611 Some(tid)
1612 }
1613 })
1614 }
1615 };
1616
1617 for seg in &chain[1..] {
1619 let ctx_type = match ¤t_type {
1620 Some(t) => t.clone(),
1621 None => return vec![],
1622 };
1623
1624 current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1625 }
1626
1627 match current_type {
1629 Some(tid) => completions_for_type(cache, &tid),
1630 None => vec![],
1631 }
1632}
1633
1634pub fn get_static_completions() -> Vec<CompletionItem> {
1637 let mut items = Vec::new();
1638
1639 for kw in SOLIDITY_KEYWORDS {
1641 items.push(CompletionItem {
1642 label: kw.to_string(),
1643 kind: Some(CompletionItemKind::KEYWORD),
1644 ..Default::default()
1645 });
1646 }
1647
1648 for (name, detail) in MAGIC_GLOBALS {
1650 items.push(CompletionItem {
1651 label: name.to_string(),
1652 kind: Some(CompletionItemKind::VARIABLE),
1653 detail: Some(detail.to_string()),
1654 ..Default::default()
1655 });
1656 }
1657
1658 for (name, detail) in GLOBAL_FUNCTIONS {
1660 items.push(CompletionItem {
1661 label: name.to_string(),
1662 kind: Some(CompletionItemKind::FUNCTION),
1663 detail: Some(detail.to_string()),
1664 ..Default::default()
1665 });
1666 }
1667
1668 for (name, detail) in ETHER_UNITS {
1670 items.push(CompletionItem {
1671 label: name.to_string(),
1672 kind: Some(CompletionItemKind::UNIT),
1673 detail: Some(detail.to_string()),
1674 ..Default::default()
1675 });
1676 }
1677
1678 for (name, detail) in TIME_UNITS {
1680 items.push(CompletionItem {
1681 label: name.to_string(),
1682 kind: Some(CompletionItemKind::UNIT),
1683 detail: Some(detail.to_string()),
1684 ..Default::default()
1685 });
1686 }
1687
1688 items
1689}
1690
1691pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1693 let mut items = cache.names.clone();
1694 items.extend(get_static_completions());
1695 items
1696}
1697
1698pub fn append_auto_import_candidates_last(
1704 mut base: Vec<CompletionItem>,
1705 mut auto_import_candidates: Vec<CompletionItem>,
1706) -> Vec<CompletionItem> {
1707 let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1708 for item in &auto_import_candidates {
1709 let entry = unique_label_edits
1710 .entry(item.label.clone())
1711 .or_insert_with(|| item.additional_text_edits.clone());
1712 if *entry != item.additional_text_edits {
1713 *entry = None;
1714 }
1715 }
1716
1717 for item in &mut base {
1721 if item.additional_text_edits.is_none()
1722 && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1723 {
1724 item.additional_text_edits = Some(edits.clone());
1725 }
1726 }
1727
1728 for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1729 if item.sort_text.is_none() {
1730 item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1731 }
1732 }
1733
1734 base.extend(auto_import_candidates);
1735 base
1736}
1737
1738pub fn top_level_importable_completion_candidates(
1743 cache: &CompletionCache,
1744 current_file_path: Option<&str>,
1745 source_text: &str,
1746) -> Vec<CompletionItem> {
1747 let mut out = Vec::new();
1748 for symbols in cache.top_level_importables_by_name.values() {
1749 for symbol in symbols {
1750 if let Some(cur) = current_file_path
1751 && cur == symbol.declaring_path
1752 {
1753 continue;
1754 }
1755
1756 let import_path = match current_file_path.and_then(|cur| {
1757 to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
1758 }) {
1759 Some(p) => p,
1760 None => continue,
1761 };
1762
1763 if import_statement_already_present(source_text, &symbol.name, &import_path) {
1764 continue;
1765 }
1766
1767 let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
1768 out.push(CompletionItem {
1769 label: symbol.name.clone(),
1770 kind: Some(symbol.kind),
1771 detail: Some(format!("{} ({import_path})", symbol.node_type)),
1772 additional_text_edits: import_edit.map(|e| vec![e]),
1773 ..Default::default()
1774 });
1775 }
1776 }
1777 out
1778}
1779
1780fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
1781 let from_dir = current_file.parent()?;
1782 let rel = pathdiff::diff_paths(target_file, from_dir)?;
1783 let mut s = rel.to_string_lossy().replace('\\', "/");
1784 if !s.starts_with("./") && !s.starts_with("../") {
1785 s = format!("./{s}");
1786 }
1787 Some(s)
1788}
1789
1790fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
1791 let named = format!("import {{{symbol}}} from \"{import_path}\";");
1792 let full = format!("import \"{import_path}\";");
1793 source_text.contains(&named) || source_text.contains(&full)
1794}
1795
1796fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
1797 let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
1798 let lines: Vec<&str> = source_text.lines().collect();
1799
1800 let last_import_line = lines
1801 .iter()
1802 .enumerate()
1803 .filter(|(_, line)| line.trim_start().starts_with("import "))
1804 .map(|(idx, _)| idx)
1805 .last();
1806
1807 let insert_line = if let Some(idx) = last_import_line {
1808 idx + 1
1809 } else if let Some(idx) = lines
1810 .iter()
1811 .enumerate()
1812 .filter(|(_, line)| line.trim_start().starts_with("pragma "))
1813 .map(|(idx, _)| idx)
1814 .last()
1815 {
1816 idx + 1
1817 } else {
1818 0
1819 };
1820
1821 Some(TextEdit {
1822 range: Range {
1823 start: Position {
1824 line: insert_line as u32,
1825 character: 0,
1826 },
1827 end: Position {
1828 line: insert_line as u32,
1829 character: 0,
1830 },
1831 },
1832 new_text: import_stmt,
1833 })
1834}
1835
1836pub fn handle_completion_with_tail_candidates(
1841 cache: Option<&CompletionCache>,
1842 source_text: &str,
1843 position: Position,
1844 trigger_char: Option<&str>,
1845 file_id: Option<FileId>,
1846 tail_candidates: Vec<CompletionItem>,
1847) -> Option<CompletionResponse> {
1848 let lines: Vec<&str> = source_text.lines().collect();
1849 let line = lines.get(position.line as usize)?;
1850
1851 let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
1853 let line_start_byte: usize = source_text[..abs_byte]
1854 .rfind('\n')
1855 .map(|i| i + 1)
1856 .unwrap_or(0);
1857 let col_byte = (abs_byte - line_start_byte) as u32;
1858
1859 let scope_ctx = file_id.map(|fid| ScopeContext {
1861 byte_pos: abs_byte,
1862 file_id: fid,
1863 });
1864
1865 let items = if trigger_char == Some(".") {
1866 let chain = parse_dot_chain(line, col_byte);
1867 if chain.is_empty() {
1868 return None;
1869 }
1870 match cache {
1871 Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
1872 None => {
1873 if chain.len() == 1 {
1875 let seg = &chain[0];
1876 if seg.name == "type" && seg.kind == AccessKind::Call {
1877 type_meta_members(seg.call_args.as_deref(), None)
1879 } else if seg.kind == AccessKind::Plain {
1880 magic_members(&seg.name).unwrap_or_default()
1881 } else {
1882 vec![]
1883 }
1884 } else {
1885 vec![]
1886 }
1887 }
1888 }
1889 } else {
1890 match cache {
1891 Some(c) => {
1892 append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
1893 }
1894 None => get_static_completions(),
1895 }
1896 };
1897
1898 Some(CompletionResponse::List(CompletionList {
1899 is_incomplete: cache.is_none(),
1900 items,
1901 }))
1902}
1903
1904pub fn handle_completion(
1914 cache: Option<&CompletionCache>,
1915 source_text: &str,
1916 position: Position,
1917 trigger_char: Option<&str>,
1918 file_id: Option<FileId>,
1919) -> Option<CompletionResponse> {
1920 handle_completion_with_tail_candidates(
1921 cache,
1922 source_text,
1923 position,
1924 trigger_char,
1925 file_id,
1926 vec![],
1927 )
1928}
1929
1930const SOLIDITY_KEYWORDS: &[&str] = &[
1931 "abstract",
1932 "address",
1933 "assembly",
1934 "bool",
1935 "break",
1936 "bytes",
1937 "bytes1",
1938 "bytes4",
1939 "bytes32",
1940 "calldata",
1941 "constant",
1942 "constructor",
1943 "continue",
1944 "contract",
1945 "delete",
1946 "do",
1947 "else",
1948 "emit",
1949 "enum",
1950 "error",
1951 "event",
1952 "external",
1953 "fallback",
1954 "false",
1955 "for",
1956 "function",
1957 "if",
1958 "immutable",
1959 "import",
1960 "indexed",
1961 "int8",
1962 "int24",
1963 "int128",
1964 "int256",
1965 "interface",
1966 "internal",
1967 "library",
1968 "mapping",
1969 "memory",
1970 "modifier",
1971 "new",
1972 "override",
1973 "payable",
1974 "pragma",
1975 "private",
1976 "public",
1977 "pure",
1978 "receive",
1979 "return",
1980 "returns",
1981 "revert",
1982 "storage",
1983 "string",
1984 "struct",
1985 "true",
1986 "type",
1987 "uint8",
1988 "uint24",
1989 "uint128",
1990 "uint160",
1991 "uint256",
1992 "unchecked",
1993 "using",
1994 "view",
1995 "virtual",
1996 "while",
1997];
1998
1999const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2001
2002const TIME_UNITS: &[(&str, &str)] = &[
2004 ("seconds", "1"),
2005 ("minutes", "60 seconds"),
2006 ("hours", "3600 seconds"),
2007 ("days", "86400 seconds"),
2008 ("weeks", "604800 seconds"),
2009];
2010
2011const MAGIC_GLOBALS: &[(&str, &str)] = &[
2012 ("msg", "msg"),
2013 ("block", "block"),
2014 ("tx", "tx"),
2015 ("abi", "abi"),
2016 ("this", "address"),
2017 ("super", "contract"),
2018 ("type", "type information"),
2019];
2020
2021const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2022 ("addmod(uint256, uint256, uint256)", "uint256"),
2024 ("mulmod(uint256, uint256, uint256)", "uint256"),
2025 ("keccak256(bytes memory)", "bytes32"),
2026 ("sha256(bytes memory)", "bytes32"),
2027 ("ripemd160(bytes memory)", "bytes20"),
2028 (
2029 "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2030 "address",
2031 ),
2032 ("blockhash(uint256 blockNumber)", "bytes32"),
2034 ("blobhash(uint256 index)", "bytes32"),
2035 ("gasleft()", "uint256"),
2036 ("assert(bool condition)", ""),
2038 ("require(bool condition)", ""),
2039 ("require(bool condition, string memory message)", ""),
2040 ("revert()", ""),
2041 ("revert(string memory reason)", ""),
2042 ("selfdestruct(address payable recipient)", ""),
2044];
2045
2046pub fn all_sol_import_paths(
2062 current_file: &Path,
2063 project_root: &Path,
2064 remappings: &[String],
2065 typed_range: Option<(u32, u32, u32)>,
2066) -> Vec<CompletionItem> {
2067 let current_dir = match current_file.parent() {
2068 Some(d) => d,
2069 None => return vec![],
2070 };
2071
2072 let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2077 .iter()
2078 .filter_map(|r| {
2079 let mut it = r.splitn(2, '=');
2080 let prefix = it.next()?.to_string();
2081 let target = it.next()?;
2082 if prefix.is_empty() || target.is_empty() {
2083 return None;
2084 }
2085 let raw = project_root.join(target);
2086 let canonical = raw.canonicalize().unwrap_or(raw);
2087 Some((prefix, canonical))
2088 })
2089 .collect();
2090
2091 let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2092 let mut items = Vec::new();
2093
2094 collect_sol_files(
2095 project_root,
2096 current_dir,
2097 &parsed_remappings,
2098 skip_dirs,
2099 typed_range,
2100 &mut items,
2101 );
2102 items.sort_by(|a, b| a.label.cmp(&b.label));
2103 items
2104}
2105
2106fn collect_sol_files(
2107 dir: &Path,
2108 current_dir: &Path,
2109 remappings: &[(String, std::path::PathBuf)],
2110 skip_dirs: &[&str],
2111 typed_range: Option<(u32, u32, u32)>,
2112 out: &mut Vec<CompletionItem>,
2113) {
2114 let entries = match std::fs::read_dir(dir) {
2115 Ok(e) => e,
2116 Err(_) => return,
2117 };
2118 for entry in entries.flatten() {
2119 let path = entry.path();
2120 let name = entry.file_name();
2121 let name_str = name.to_string_lossy();
2122
2123 if name_str.starts_with('.') {
2124 continue;
2125 }
2126
2127 if path.is_dir() {
2128 if skip_dirs.contains(&name_str.as_ref()) {
2129 continue;
2130 }
2131 collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2132 continue;
2133 }
2134
2135 if !path.is_file() || !name_str.ends_with(".sol") {
2136 continue;
2137 }
2138
2139 if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2141 let s = rel.to_string_lossy().to_string();
2142 let label = if s.starts_with("../") || s.starts_with("./") {
2143 s
2144 } else {
2145 format!("./{s}")
2146 };
2147 out.push(make_import_item(label, typed_range));
2148 }
2149
2150 for (prefix, target_abs) in remappings {
2156 if let Ok(suffix) = path.strip_prefix(target_abs) {
2157 let suffix_str = suffix
2158 .to_string_lossy()
2159 .trim_start_matches(['/', '\\'])
2160 .to_string();
2161 let label = format!("{}{}", prefix, suffix_str);
2162 out.push(make_import_item(label, typed_range));
2163 }
2164 }
2165 }
2166}
2167
2168fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2169 let text_edit = typed_range.map(|(line, start_col, end_col)| {
2176 tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2177 range: Range {
2178 start: Position {
2179 line,
2180 character: start_col,
2181 },
2182 end: Position {
2183 line,
2184 character: end_col,
2185 },
2186 },
2187 new_text: label.clone(),
2188 })
2189 });
2190 CompletionItem {
2191 label: label.clone(),
2192 filter_text: Some(label.clone()),
2193 text_edit,
2194 kind: Some(CompletionItemKind::FILE),
2195 ..Default::default()
2196 }
2197}
2198
2199#[cfg(test)]
2200mod tests {
2201 use super::{
2202 CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2203 build_completion_cache, extract_top_level_importables_for_file,
2204 };
2205 use serde_json::json;
2206 use std::collections::HashMap;
2207 use tower_lsp::lsp_types::CompletionItemKind;
2208 use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2209
2210 fn empty_cache() -> CompletionCache {
2211 CompletionCache {
2212 names: vec![],
2213 name_to_type: HashMap::new(),
2214 node_members: HashMap::new(),
2215 type_to_node: HashMap::new(),
2216 name_to_node_id: HashMap::new(),
2217 method_identifiers: HashMap::new(),
2218 function_return_types: HashMap::new(),
2219 using_for: HashMap::new(),
2220 using_for_wildcard: vec![],
2221 general_completions: vec![],
2222 scope_declarations: HashMap::new(),
2223 scope_parent: HashMap::new(),
2224 scope_ranges: vec![],
2225 path_to_file_id: HashMap::new(),
2226 linearized_base_contracts: HashMap::new(),
2227 contract_kinds: HashMap::new(),
2228 top_level_importables_by_name: HashMap::new(),
2229 top_level_importables_by_file: HashMap::new(),
2230 }
2231 }
2232
2233 #[test]
2234 fn top_level_importables_include_only_direct_declared_symbols() {
2235 let sources = json!({
2236 "/tmp/A.sol": {
2237 "id": 0,
2238 "ast": {
2239 "id": 1,
2240 "nodeType": "SourceUnit",
2241 "src": "0:100:0",
2242 "nodes": [
2243 { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2244 { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2245 { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2246 ] },
2247 { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2248 { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2249 { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2250 { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2251 { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2252 { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2253 ]
2254 }
2255 }
2256 });
2257
2258 let cache = build_completion_cache(&sources, None);
2259 let map = &cache.top_level_importables_by_name;
2260 let by_file = &cache.top_level_importables_by_file;
2261
2262 assert!(map.contains_key("C"));
2263 assert!(map.contains_key("S"));
2264 assert!(map.contains_key("E"));
2265 assert!(map.contains_key("Wad"));
2266 assert!(map.contains_key("freeFn"));
2267 assert!(map.contains_key("TOP_CONST"));
2268
2269 assert!(!map.contains_key("Alias"));
2270 assert!(!map.contains_key("inside"));
2271 assert!(!map.contains_key("TOP_VAR"));
2272
2273 let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2274 let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2275 assert!(file_names.contains(&"C"));
2276 assert!(file_names.contains(&"TOP_CONST"));
2277 assert!(!file_names.contains(&"Alias"));
2278 }
2279
2280 #[test]
2281 fn top_level_importables_keep_multiple_declarations_for_same_name() {
2282 let sources = json!({
2283 "/tmp/A.sol": {
2284 "id": 0,
2285 "ast": {
2286 "id": 1,
2287 "nodeType": "SourceUnit",
2288 "src": "0:100:0",
2289 "nodes": [
2290 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2291 ]
2292 }
2293 },
2294 "/tmp/B.sol": {
2295 "id": 1,
2296 "ast": {
2297 "id": 2,
2298 "nodeType": "SourceUnit",
2299 "src": "0:100:1",
2300 "nodes": [
2301 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2302 ]
2303 }
2304 }
2305 });
2306
2307 let cache = build_completion_cache(&sources, None);
2308 let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2309 assert_eq!(entries.len(), 2);
2310 }
2311
2312 #[test]
2313 fn extract_top_level_importables_for_file_finds_expected_symbols() {
2314 let ast = json!({
2315 "id": 1,
2316 "nodeType": "SourceUnit",
2317 "src": "0:100:0",
2318 "nodes": [
2319 { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2320 { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2321 { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2322 ]
2323 });
2324
2325 let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2326 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2327 assert!(names.contains(&"f"));
2328 assert!(names.contains(&"K"));
2329 assert!(!names.contains(&"V"));
2330 }
2331
2332 #[test]
2333 fn top_level_importables_can_be_replaced_per_file() {
2334 let sources = json!({
2335 "/tmp/A.sol": {
2336 "id": 0,
2337 "ast": {
2338 "id": 1,
2339 "nodeType": "SourceUnit",
2340 "src": "0:100:0",
2341 "nodes": [
2342 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2343 ]
2344 }
2345 },
2346 "/tmp/B.sol": {
2347 "id": 1,
2348 "ast": {
2349 "id": 2,
2350 "nodeType": "SourceUnit",
2351 "src": "0:100:1",
2352 "nodes": [
2353 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2354 ]
2355 }
2356 }
2357 });
2358
2359 let mut cache = build_completion_cache(&sources, None);
2360 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2361
2362 cache.replace_top_level_importables_for_path(
2363 "/tmp/A.sol".to_string(),
2364 vec![TopLevelImportable {
2365 name: "newA".to_string(),
2366 declaring_path: "/tmp/A.sol".to_string(),
2367 node_type: "FunctionDefinition".to_string(),
2368 kind: CompletionItemKind::FUNCTION,
2369 }],
2370 );
2371 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2372 assert!(cache.top_level_importables_by_name.contains_key("newA"));
2373
2374 cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2375 assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2376 }
2377
2378 #[test]
2379 fn append_auto_import_candidates_last_sets_tail_sort_text() {
2380 let base = vec![CompletionItem {
2381 label: "localVar".to_string(),
2382 ..Default::default()
2383 }];
2384 let auto = vec![CompletionItem {
2385 label: "ImportMe".to_string(),
2386 ..Default::default()
2387 }];
2388
2389 let out = append_auto_import_candidates_last(base, auto);
2390 assert_eq!(out.len(), 2);
2391 assert_eq!(out[1].label, "ImportMe");
2392 assert!(
2393 out[1]
2394 .sort_text
2395 .as_deref()
2396 .is_some_and(|s| s.starts_with("zz_autoimport_"))
2397 );
2398 }
2399
2400 #[test]
2401 fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2402 let base = vec![CompletionItem {
2403 label: "B".to_string(),
2404 ..Default::default()
2405 }];
2406 let auto = vec![
2407 CompletionItem {
2408 label: "B".to_string(),
2409 detail: Some("ContractDefinition (./B.sol)".to_string()),
2410 ..Default::default()
2411 },
2412 CompletionItem {
2413 label: "B".to_string(),
2414 detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2415 ..Default::default()
2416 },
2417 ];
2418
2419 let out = append_auto_import_candidates_last(base, auto);
2420 assert_eq!(out.len(), 3);
2421 }
2422
2423 #[test]
2424 fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2425 let base = vec![CompletionItem {
2426 label: "B".to_string(),
2427 ..Default::default()
2428 }];
2429 let auto = vec![CompletionItem {
2430 label: "B".to_string(),
2431 additional_text_edits: Some(vec![TextEdit {
2432 range: Range {
2433 start: Position {
2434 line: 0,
2435 character: 0,
2436 },
2437 end: Position {
2438 line: 0,
2439 character: 0,
2440 },
2441 },
2442 new_text: "import {B} from \"./B.sol\";\n".to_string(),
2443 }]),
2444 ..Default::default()
2445 }];
2446 let out = append_auto_import_candidates_last(base, auto);
2447 assert!(
2448 out[0].additional_text_edits.is_some(),
2449 "base item should inherit unique import edit"
2450 );
2451 }
2452
2453 #[test]
2454 fn top_level_importable_candidates_include_import_edit() {
2455 let mut cache = empty_cache();
2456 cache.top_level_importables_by_name.insert(
2457 "B".to_string(),
2458 vec![TopLevelImportable {
2459 name: "B".to_string(),
2460 declaring_path: "/tmp/example/B.sol".to_string(),
2461 node_type: "ContractDefinition".to_string(),
2462 kind: CompletionItemKind::CLASS,
2463 }],
2464 );
2465
2466 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2467 let items = super::top_level_importable_completion_candidates(
2468 &cache,
2469 Some("/tmp/example/A.sol"),
2470 source,
2471 );
2472 assert_eq!(items.len(), 1);
2473 let edit_text = items[0]
2474 .additional_text_edits
2475 .as_ref()
2476 .and_then(|edits| edits.first())
2477 .map(|e| e.new_text.clone())
2478 .unwrap_or_default();
2479 assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2480 }
2481
2482 #[test]
2483 fn handle_completion_general_path_keeps_base_items() {
2484 let mut cache = empty_cache();
2485 cache.general_completions = vec![CompletionItem {
2486 label: "A".to_string(),
2487 ..Default::default()
2488 }];
2489
2490 let resp = super::handle_completion(
2491 Some(&cache),
2492 "contract X {}",
2493 Position {
2494 line: 0,
2495 character: 0,
2496 },
2497 None,
2498 None,
2499 );
2500 match resp {
2501 Some(CompletionResponse::List(list)) => {
2502 assert_eq!(list.items.len(), 1);
2503 assert_eq!(list.items[0].label, "A");
2504 }
2505 _ => panic!("expected completion list"),
2506 }
2507 }
2508
2509 #[test]
2512 fn make_import_item_has_filter_text_and_text_edit() {
2513 let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
2514 assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
2515 assert!(item.text_edit.is_some(), "text_edit should be set");
2516 assert!(
2517 item.insert_text.is_none(),
2518 "insert_text should be absent when text_edit is set"
2519 );
2520 }
2521
2522 #[test]
2523 fn all_sol_import_paths_no_panic_missing_dir() {
2524 let current = std::path::Path::new("/nonexistent/src/Foo.sol");
2525 let root = std::path::Path::new("/nonexistent");
2526 let items = super::all_sol_import_paths(current, root, &[], None);
2527 assert!(items.is_empty());
2528 }
2529
2530 #[test]
2531 fn all_sol_import_paths_relative_labels() {
2532 let root = std::path::Path::new("example");
2534 let current = root.join("A.sol");
2535 let items = super::all_sol_import_paths(¤t, root, &[], None);
2536 for item in &items {
2538 assert!(
2539 item.label.starts_with("./") || item.label.starts_with("../"),
2540 "label should be relative: {}",
2541 item.label
2542 );
2543 assert!(
2544 item.label.ends_with(".sol"),
2545 "label should end with .sol: {}",
2546 item.label
2547 );
2548 }
2549 assert!(!items.is_empty(), "should find at least one .sol file");
2550 }
2551}