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(
313 path: &str,
314 ast: &Value,
315) -> Vec<TopLevelImportable> {
316 let mut out: Vec<TopLevelImportable> = Vec::new();
317 let mut stack: Vec<&Value> = vec![ast];
318 let mut source_unit_id: Option<NodeId> = None;
319
320 while let Some(tree) = stack.pop() {
321 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
322 let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
323 if node_type == "SourceUnit" {
324 source_unit_id = node_id;
325 }
326 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
327
328 if !name.is_empty()
329 && is_top_level_importable_decl(node_type, tree)
330 && let Some(src_scope) = source_unit_id
331 && tree.get("scope").and_then(|v| v.as_u64()) == Some(src_scope.0)
332 {
333 out.push(TopLevelImportable {
334 name: name.to_string(),
335 declaring_path: path.to_string(),
336 node_type: node_type.to_string(),
337 kind: node_type_to_completion_kind(node_type),
338 });
339 }
340
341 for key in CHILD_KEYS {
342 push_if_node_or_array(tree, key, &mut stack);
343 }
344 }
345
346 out
347}
348
349impl CompletionCache {
350 pub fn replace_top_level_importables_for_path(
353 &mut self,
354 path: String,
355 symbols: Vec<TopLevelImportable>,
356 ) {
357 self.top_level_importables_by_file.insert(path, symbols);
358 self.top_level_importables_by_name =
359 build_top_level_importables_by_name(&self.top_level_importables_by_file);
360 }
361}
362
363pub fn build_completion_cache(sources: &Value, contracts: Option<&Value>) -> CompletionCache {
366 let source_count = sources.as_object().map_or(0, |obj| obj.len());
367 let est_names = source_count * 20;
370 let est_contracts = source_count * 5;
371
372 let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
373 let mut seen_names: HashMap<String, usize> = HashMap::with_capacity(est_names);
374 let mut name_to_type: HashMap<String, String> = HashMap::with_capacity(est_names);
375 let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
376 HashMap::with_capacity(est_contracts);
377 let mut type_to_node: HashMap<String, NodeId> = HashMap::with_capacity(est_contracts);
378 let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
379 HashMap::with_capacity(est_contracts);
380 let mut name_to_node_id: HashMap<String, NodeId> = HashMap::with_capacity(est_names);
381 let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
382
383 let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
385
386 let mut function_signatures: HashMap<NodeId, HashMap<String, Vec<String>>> =
388 HashMap::with_capacity(est_contracts);
389
390 let mut function_return_types: HashMap<(NodeId, String), String> =
392 HashMap::with_capacity(source_count * 10);
393
394 let mut using_for: HashMap<String, Vec<CompletionItem>> = HashMap::with_capacity(source_count);
396 let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
397
398 let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
400
401 let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
403 HashMap::with_capacity(est_contracts);
404 let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
405 let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
406 let mut path_to_file_id: HashMap<String, FileId> = HashMap::with_capacity(source_count);
407 let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
408 HashMap::with_capacity(est_contracts);
409 let mut top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>> =
410 HashMap::with_capacity(est_names);
411
412 if let Some(sources_obj) = sources.as_object() {
413 for (path, source_data) in sources_obj {
414 if let Some(ast) = source_data.get("ast") {
415 if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
417 path_to_file_id.insert(path.clone(), FileId(fid));
418 }
419 let file_importables = extract_top_level_importables_for_file(path, ast);
420 if !file_importables.is_empty() {
421 top_level_importables_by_file.insert(path.clone(), file_importables);
422 }
423 let mut stack: Vec<&Value> = vec![ast];
424
425 while let Some(tree) = stack.pop() {
426 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
427 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
428 let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
429
430 let is_scope_node = matches!(
435 node_type,
436 "SourceUnit"
437 | "ContractDefinition"
438 | "FunctionDefinition"
439 | "ModifierDefinition"
440 | "Block"
441 | "UncheckedBlock"
442 );
443 if is_scope_node && let Some(nid) = node_id {
444 if let Some(src_loc) = parse_src(tree) {
445 scope_ranges.push(ScopeRange {
446 node_id: nid,
447 start: src_loc.offset,
448 end: src_loc.end(),
449 file_id: src_loc.file_id,
450 });
451 }
452 if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_u64()) {
454 scope_parent.insert(nid, NodeId(parent_id));
455 }
456 }
457
458 if node_type == "ContractDefinition"
460 && let Some(nid) = node_id
461 && let Some(bases) = tree
462 .get("linearizedBaseContracts")
463 .and_then(|v| v.as_array())
464 {
465 let base_ids: Vec<NodeId> = bases
466 .iter()
467 .filter_map(|b| b.as_u64())
468 .map(NodeId)
469 .collect();
470 if !base_ids.is_empty() {
471 linearized_base_contracts.insert(nid, base_ids);
472 }
473 }
474
475 if node_type == "VariableDeclaration"
477 && !name.is_empty()
478 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
479 && let Some(tid) = tree
480 .get("typeDescriptions")
481 .and_then(|td| td.get("typeIdentifier"))
482 .and_then(|v| v.as_str())
483 {
484 scope_declarations
485 .entry(NodeId(scope_raw))
486 .or_default()
487 .push(ScopedDeclaration {
488 name: name.to_string(),
489 type_id: tid.to_string(),
490 });
491 }
492
493 if node_type == "FunctionDefinition"
495 && !name.is_empty()
496 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
497 && let Some(tid) = tree
498 .get("typeDescriptions")
499 .and_then(|td| td.get("typeIdentifier"))
500 .and_then(|v| v.as_str())
501 {
502 scope_declarations
503 .entry(NodeId(scope_raw))
504 .or_default()
505 .push(ScopedDeclaration {
506 name: name.to_string(),
507 type_id: tid.to_string(),
508 });
509 }
510
511 if !name.is_empty() && !seen_names.contains_key(name) {
513 let type_string = tree
514 .get("typeDescriptions")
515 .and_then(|td| td.get("typeString"))
516 .and_then(|v| v.as_str())
517 .map(|s| s.to_string());
518
519 let type_id = tree
520 .get("typeDescriptions")
521 .and_then(|td| td.get("typeIdentifier"))
522 .and_then(|v| v.as_str());
523
524 let kind = node_type_to_completion_kind(node_type);
525
526 let item = CompletionItem {
527 label: name.to_string(),
528 kind: Some(kind),
529 detail: type_string,
530 ..Default::default()
531 };
532
533 let idx = names.len();
534 names.push(item);
535 seen_names.insert(name.to_string(), idx);
536
537 if let Some(tid) = type_id {
539 name_to_type.insert(name.to_string(), tid.to_string());
540 }
541 }
542
543 if node_type == "StructDefinition"
545 && let Some(id) = node_id
546 {
547 let mut members = Vec::new();
548 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
549 for member in member_array {
550 let member_name =
551 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
552 if member_name.is_empty() {
553 continue;
554 }
555 let member_type = member
556 .get("typeDescriptions")
557 .and_then(|td| td.get("typeString"))
558 .and_then(|v| v.as_str())
559 .map(|s| s.to_string());
560
561 members.push(CompletionItem {
562 label: member_name.to_string(),
563 kind: Some(CompletionItemKind::FIELD),
564 detail: member_type,
565 ..Default::default()
566 });
567 }
568 }
569 if !members.is_empty() {
570 node_members.insert(id, members);
571 }
572
573 if let Some(tid) = tree
575 .get("typeDescriptions")
576 .and_then(|td| td.get("typeIdentifier"))
577 .and_then(|v| v.as_str())
578 {
579 type_to_node.insert(tid.to_string(), id);
580 }
581 }
582
583 if node_type == "ContractDefinition"
585 && let Some(id) = node_id
586 {
587 let mut members = Vec::new();
588 let mut fn_sigs: HashMap<String, Vec<String>> = HashMap::new();
589 if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
590 for member in nodes_array {
591 let member_type = member
592 .get("nodeType")
593 .and_then(|v| v.as_str())
594 .unwrap_or("");
595 let member_name =
596 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
597 if member_name.is_empty() {
598 continue;
599 }
600
601 let (member_detail, label_details) =
603 if member_type == "FunctionDefinition" {
604 if let Some(ret_params) = member
608 .get("returnParameters")
609 .and_then(|rp| rp.get("parameters"))
610 .and_then(|v| v.as_array())
611 && ret_params.len() == 1
612 && let Some(ret_tid) = ret_params[0]
613 .get("typeDescriptions")
614 .and_then(|td| td.get("typeIdentifier"))
615 .and_then(|v| v.as_str())
616 {
617 function_return_types.insert(
618 (id, member_name.to_string()),
619 ret_tid.to_string(),
620 );
621 }
622
623 if let Some(sig) = build_function_signature(member) {
624 fn_sigs
625 .entry(member_name.to_string())
626 .or_default()
627 .push(sig.clone());
628 (Some(sig), None)
629 } else {
630 (
631 member
632 .get("typeDescriptions")
633 .and_then(|td| td.get("typeString"))
634 .and_then(|v| v.as_str())
635 .map(|s| s.to_string()),
636 None,
637 )
638 }
639 } else {
640 (
641 member
642 .get("typeDescriptions")
643 .and_then(|td| td.get("typeString"))
644 .and_then(|v| v.as_str())
645 .map(|s| s.to_string()),
646 None,
647 )
648 };
649
650 let kind = node_type_to_completion_kind(member_type);
651 members.push(CompletionItem {
652 label: member_name.to_string(),
653 kind: Some(kind),
654 detail: member_detail,
655 label_details,
656 ..Default::default()
657 });
658 }
659 }
660 if !members.is_empty() {
661 node_members.insert(id, members);
662 }
663 if !fn_sigs.is_empty() {
664 function_signatures.insert(id, fn_sigs);
665 }
666
667 if let Some(tid) = tree
668 .get("typeDescriptions")
669 .and_then(|td| td.get("typeIdentifier"))
670 .and_then(|v| v.as_str())
671 {
672 type_to_node.insert(tid.to_string(), id);
673 }
674
675 if !name.is_empty() {
677 contract_locations.push((path.clone(), name.to_string(), id));
678 name_to_node_id.insert(name.to_string(), id);
679 }
680
681 if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
683 contract_kinds.insert(id, ck.to_string());
684 }
685 }
686
687 if node_type == "EnumDefinition"
689 && let Some(id) = node_id
690 {
691 let mut members = Vec::new();
692 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
693 for member in member_array {
694 let member_name =
695 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
696 if member_name.is_empty() {
697 continue;
698 }
699 members.push(CompletionItem {
700 label: member_name.to_string(),
701 kind: Some(CompletionItemKind::ENUM_MEMBER),
702 detail: None,
703 ..Default::default()
704 });
705 }
706 }
707 if !members.is_empty() {
708 node_members.insert(id, members);
709 }
710
711 if let Some(tid) = tree
712 .get("typeDescriptions")
713 .and_then(|td| td.get("typeIdentifier"))
714 .and_then(|v| v.as_str())
715 {
716 type_to_node.insert(tid.to_string(), id);
717 }
718 }
719
720 if node_type == "UsingForDirective" {
722 let target_type = tree.get("typeName").and_then(|tn| {
724 tn.get("typeDescriptions")
725 .and_then(|td| td.get("typeIdentifier"))
726 .and_then(|v| v.as_str())
727 .map(|s| s.to_string())
728 });
729
730 if let Some(lib) = tree.get("libraryName") {
732 if let Some(lib_id) =
733 lib.get("referencedDeclaration").and_then(|v| v.as_u64())
734 {
735 using_for_directives.push((NodeId(lib_id), target_type));
736 }
737 }
738 else if let Some(func_list) =
742 tree.get("functionList").and_then(|v| v.as_array())
743 {
744 for entry in func_list {
745 if entry.get("operator").is_some() {
747 continue;
748 }
749 if let Some(def) = entry.get("definition") {
750 let fn_name =
751 def.get("name").and_then(|v| v.as_str()).unwrap_or("");
752 if !fn_name.is_empty() {
753 let items = if let Some(ref tid) = target_type {
754 using_for.entry(tid.clone()).or_default()
755 } else {
756 &mut using_for_wildcard
757 };
758 items.push(CompletionItem {
759 label: fn_name.to_string(),
760 kind: Some(CompletionItemKind::FUNCTION),
761 detail: None,
762 ..Default::default()
763 });
764 }
765 }
766 }
767 }
768 }
769
770 for key in CHILD_KEYS {
772 push_if_node_or_array(tree, key, &mut stack);
773 }
774 }
775 }
776 }
777 }
778
779 for (lib_id, target_type) in &using_for_directives {
782 if let Some(lib_members) = node_members.get(lib_id) {
783 let items: Vec<CompletionItem> = lib_members
784 .iter()
785 .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
786 .cloned()
787 .collect();
788 if !items.is_empty() {
789 if let Some(tid) = target_type {
790 using_for.entry(tid.clone()).or_default().extend(items);
791 } else {
792 using_for_wildcard.extend(items);
793 }
794 }
795 }
796 }
797
798 if let Some(contracts_val) = contracts
800 && let Some(contracts_obj) = contracts_val.as_object()
801 {
802 for (path, contract_name, node_id) in &contract_locations {
803 let fn_sigs = function_signatures.get(node_id);
805
806 if let Some(path_entry) = contracts_obj.get(path)
807 && let Some(contract_entry) = path_entry.get(contract_name)
808 && let Some(evm) = contract_entry.get("evm")
809 && let Some(methods) = evm.get("methodIdentifiers")
810 && let Some(methods_obj) = methods.as_object()
811 {
812 let mut items: Vec<CompletionItem> = Vec::new();
813 for (signature, selector_val) in methods_obj {
814 let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
817 let selector_str = selector_val
818 .as_str()
819 .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
820 .unwrap_or_default();
821
822 let description =
824 fn_sigs
825 .and_then(|sigs| sigs.get(&fn_name))
826 .and_then(|sig_list| {
827 if sig_list.len() == 1 {
828 Some(sig_list[0].clone())
830 } else {
831 let abi_param_count = count_abi_params(signature);
833 sig_list
834 .iter()
835 .find(|s| count_signature_params(s) == abi_param_count)
836 .cloned()
837 }
838 });
839
840 items.push(CompletionItem {
841 label: fn_name,
842 kind: Some(CompletionItemKind::FUNCTION),
843 detail: Some(signature.clone()),
844 label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
845 detail: Some(selector_str),
846 description,
847 }),
848 ..Default::default()
849 });
850 }
851 if !items.is_empty() {
852 method_identifiers.insert(*node_id, items);
853 }
854 }
855 }
856 }
857
858 let mut general_completions = names.clone();
860 general_completions.extend(get_static_completions());
861
862 scope_ranges.sort_by_key(|r| r.end - r.start);
864
865 let orphan_ids: Vec<NodeId> = scope_ranges
871 .iter()
872 .filter(|r| !scope_parent.contains_key(&r.node_id))
873 .map(|r| r.node_id)
874 .collect();
875 let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
877 .iter()
878 .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
879 .collect();
880 for orphan_id in &orphan_ids {
881 if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
882 let parent = scope_ranges
885 .iter()
886 .find(|r| {
887 r.node_id != *orphan_id
888 && r.file_id == file_id
889 && r.start <= start
890 && r.end >= end
891 && (r.end - r.start) > (end - start)
892 })
893 .map(|r| r.node_id);
894 if let Some(parent_id) = parent {
895 scope_parent.insert(*orphan_id, parent_id);
896 }
897 }
898 }
899
900 let top_level_importables_by_name =
901 build_top_level_importables_by_name(&top_level_importables_by_file);
902
903 CompletionCache {
904 names,
905 name_to_type,
906 node_members,
907 type_to_node,
908 name_to_node_id,
909 method_identifiers,
910 function_return_types,
911 using_for,
912 using_for_wildcard,
913 general_completions,
914 scope_declarations,
915 scope_parent,
916 scope_ranges,
917 path_to_file_id,
918 linearized_base_contracts,
919 contract_kinds,
920 top_level_importables_by_name,
921 top_level_importables_by_file,
922 }
923}
924
925fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
927 let items = match name {
928 "msg" => vec![
929 ("data", "bytes calldata"),
930 ("sender", "address"),
931 ("sig", "bytes4"),
932 ("value", "uint256"),
933 ],
934 "block" => vec![
935 ("basefee", "uint256"),
936 ("blobbasefee", "uint256"),
937 ("chainid", "uint256"),
938 ("coinbase", "address payable"),
939 ("difficulty", "uint256"),
940 ("gaslimit", "uint256"),
941 ("number", "uint256"),
942 ("prevrandao", "uint256"),
943 ("timestamp", "uint256"),
944 ],
945 "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
946 "abi" => vec![
947 ("decode(bytes memory, (...))", "..."),
948 ("encode(...)", "bytes memory"),
949 ("encodePacked(...)", "bytes memory"),
950 ("encodeWithSelector(bytes4, ...)", "bytes memory"),
951 ("encodeWithSignature(string memory, ...)", "bytes memory"),
952 ("encodeCall(function, (...))", "bytes memory"),
953 ],
954 "type" => vec![
957 ("name", "string"),
958 ("creationCode", "bytes memory"),
959 ("runtimeCode", "bytes memory"),
960 ("interfaceId", "bytes4"),
961 ("min", "T"),
962 ("max", "T"),
963 ],
964 "bytes" => vec![("concat(...)", "bytes memory")],
966 "string" => vec![("concat(...)", "string memory")],
967 _ => return None,
968 };
969
970 Some(
971 items
972 .into_iter()
973 .map(|(label, detail)| CompletionItem {
974 label: label.to_string(),
975 kind: Some(CompletionItemKind::PROPERTY),
976 detail: Some(detail.to_string()),
977 ..Default::default()
978 })
979 .collect(),
980 )
981}
982
983#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986enum TypeMetaKind {
987 Contract,
989 Interface,
991 IntegerType,
993 Unknown,
995}
996
997fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
999 if arg == "int" || arg == "uint" {
1001 return TypeMetaKind::IntegerType;
1002 }
1003 if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1004 && let Ok(n) = suffix.parse::<u16>()
1005 && (8..=256).contains(&n)
1006 && n % 8 == 0
1007 {
1008 return TypeMetaKind::IntegerType;
1009 }
1010
1011 if let Some(c) = cache
1013 && let Some(&node_id) = c.name_to_node_id.get(arg)
1014 {
1015 return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1016 Some("interface") => TypeMetaKind::Interface,
1017 Some("library") => TypeMetaKind::Contract, _ => TypeMetaKind::Contract,
1019 };
1020 }
1021
1022 TypeMetaKind::Unknown
1023}
1024
1025fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1027 let kind = match arg {
1028 Some(a) => classify_type_arg(a, cache),
1029 None => TypeMetaKind::Unknown,
1030 };
1031
1032 let items: Vec<(&str, &str)> = match kind {
1033 TypeMetaKind::Contract => vec![
1034 ("name", "string"),
1035 ("creationCode", "bytes memory"),
1036 ("runtimeCode", "bytes memory"),
1037 ],
1038 TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1039 TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1040 TypeMetaKind::Unknown => vec![
1041 ("name", "string"),
1042 ("creationCode", "bytes memory"),
1043 ("runtimeCode", "bytes memory"),
1044 ("interfaceId", "bytes4"),
1045 ("min", "T"),
1046 ("max", "T"),
1047 ],
1048 };
1049
1050 items
1051 .into_iter()
1052 .map(|(label, detail)| CompletionItem {
1053 label: label.to_string(),
1054 kind: Some(CompletionItemKind::PROPERTY),
1055 detail: Some(detail.to_string()),
1056 ..Default::default()
1057 })
1058 .collect()
1059}
1060
1061fn address_members() -> Vec<CompletionItem> {
1063 [
1064 ("balance", "uint256", CompletionItemKind::PROPERTY),
1065 ("code", "bytes memory", CompletionItemKind::PROPERTY),
1066 ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1067 ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1068 ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1069 (
1070 "call(bytes memory)",
1071 "(bool, bytes memory)",
1072 CompletionItemKind::FUNCTION,
1073 ),
1074 (
1075 "delegatecall(bytes memory)",
1076 "(bool, bytes memory)",
1077 CompletionItemKind::FUNCTION,
1078 ),
1079 (
1080 "staticcall(bytes memory)",
1081 "(bool, bytes memory)",
1082 CompletionItemKind::FUNCTION,
1083 ),
1084 ]
1085 .iter()
1086 .map(|(label, detail, kind)| CompletionItem {
1087 label: label.to_string(),
1088 kind: Some(*kind),
1089 detail: if detail.is_empty() {
1090 None
1091 } else {
1092 Some(detail.to_string())
1093 },
1094 ..Default::default()
1095 })
1096 .collect()
1097}
1098
1099#[derive(Debug, Clone, PartialEq)]
1101pub enum AccessKind {
1102 Plain,
1104 Call,
1106 Index,
1108}
1109
1110#[derive(Debug, Clone, PartialEq)]
1112pub struct DotSegment {
1113 pub name: String,
1114 pub kind: AccessKind,
1115 pub call_args: Option<String>,
1118}
1119
1120fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1124 let close = bytes[pos];
1125 let open = match close {
1126 b')' => b'(',
1127 b']' => b'[',
1128 _ => return pos,
1129 };
1130 let mut depth = 1u32;
1131 let mut i = pos;
1132 while i > 0 && depth > 0 {
1133 i -= 1;
1134 if bytes[i] == close {
1135 depth += 1;
1136 } else if bytes[i] == open {
1137 depth -= 1;
1138 }
1139 }
1140 i
1141}
1142
1143pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1148 let col = character as usize;
1149 if col == 0 {
1150 return vec![];
1151 }
1152
1153 let bytes = line.as_bytes();
1154 let mut segments: Vec<DotSegment> = Vec::new();
1155
1156 let mut pos = col;
1158 if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1159 pos -= 1;
1160 }
1161
1162 loop {
1163 if pos == 0 {
1164 break;
1165 }
1166
1167 let (kind, call_args) = if bytes[pos - 1] == b')' {
1169 let close = pos - 1; pos = skip_brackets_backwards(bytes, close);
1171 let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1173 .trim()
1174 .to_string();
1175 let args = if args_text.is_empty() {
1176 None
1177 } else {
1178 Some(args_text)
1179 };
1180 (AccessKind::Call, args)
1181 } else if bytes[pos - 1] == b']' {
1182 pos -= 1; pos = skip_brackets_backwards(bytes, pos);
1184 (AccessKind::Index, None)
1185 } else {
1186 (AccessKind::Plain, None)
1187 };
1188
1189 let end = pos;
1191 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1192 pos -= 1;
1193 }
1194
1195 if pos == end {
1196 break;
1198 }
1199
1200 let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1201 segments.push(DotSegment {
1202 name,
1203 kind,
1204 call_args,
1205 });
1206
1207 if pos > 0 && bytes[pos - 1] == b'.' {
1209 pos -= 1; } else {
1211 break;
1212 }
1213 }
1214
1215 segments.reverse(); segments
1217}
1218
1219pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1222 let segments = parse_dot_chain(line, character);
1223 segments.last().map(|s| s.name.clone())
1224}
1225
1226#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1227Solidity AST uses different suffixes in different contexts:
1228 - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1229 - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1230 - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1231All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1232fn strip_type_suffix(type_id: &str) -> &str {
1233 let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1234 s.strip_suffix("_storage")
1235 .or_else(|| s.strip_suffix("_memory"))
1236 .or_else(|| s.strip_suffix("_calldata"))
1237 .unwrap_or(s)
1238}
1239
1240fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1244 if let Some(items) = cache.using_for.get(type_id) {
1246 return items.clone();
1247 }
1248
1249 let base = strip_type_suffix(type_id);
1251 let variants = [
1252 base.to_string(),
1253 format!("{}_storage", base),
1254 format!("{}_storage_ptr", base),
1255 format!("{}_memory", base),
1256 format!("{}_memory_ptr", base),
1257 format!("{}_calldata", base),
1258 ];
1259 for variant in &variants {
1260 if variant.as_str() != type_id
1261 && let Some(items) = cache.using_for.get(variant.as_str())
1262 {
1263 return items.clone();
1264 }
1265 }
1266
1267 vec![]
1268}
1269
1270fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1273 if type_id == "t_address" || type_id == "t_address_payable" {
1275 let mut items = address_members();
1276 if let Some(uf) = cache.using_for.get(type_id) {
1278 items.extend(uf.iter().cloned());
1279 }
1280 items.extend(cache.using_for_wildcard.iter().cloned());
1281 return items;
1282 }
1283
1284 let resolved_node_id = extract_node_id_from_type(type_id)
1285 .or_else(|| cache.type_to_node.get(type_id).copied())
1286 .or_else(|| {
1287 type_id
1289 .strip_prefix("__node_id_")
1290 .and_then(|s| s.parse::<u64>().ok())
1291 .map(NodeId)
1292 });
1293
1294 let mut items = Vec::new();
1295 let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1296
1297 if let Some(node_id) = resolved_node_id {
1298 if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1300 for item in method_items {
1301 seen_labels.insert(item.label.clone());
1302 items.push(item.clone());
1303 }
1304 }
1305
1306 if let Some(members) = cache.node_members.get(&node_id) {
1308 for item in members {
1309 if !seen_labels.contains(&item.label) {
1310 seen_labels.insert(item.label.clone());
1311 items.push(item.clone());
1312 }
1313 }
1314 }
1315 }
1316
1317 let is_contract_name = resolved_node_id
1321 .map(|nid| cache.contract_kinds.contains_key(&nid))
1322 .unwrap_or(false);
1323
1324 if !is_contract_name {
1325 let uf_items = lookup_using_for(cache, type_id);
1327 for item in &uf_items {
1328 if !seen_labels.contains(&item.label) {
1329 seen_labels.insert(item.label.clone());
1330 items.push(item.clone());
1331 }
1332 }
1333
1334 for item in &cache.using_for_wildcard {
1336 if !seen_labels.contains(&item.label) {
1337 seen_labels.insert(item.label.clone());
1338 items.push(item.clone());
1339 }
1340 }
1341 }
1342
1343 items
1344}
1345
1346fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1348 if let Some(tid) = cache.name_to_type.get(name) {
1350 return Some(tid.clone());
1351 }
1352 if let Some(node_id) = cache.name_to_node_id.get(name) {
1354 for (tid, nid) in &cache.type_to_node {
1356 if nid == node_id {
1357 return Some(tid.clone());
1358 }
1359 }
1360 return Some(format!("__node_id_{}", node_id));
1362 }
1363 None
1364}
1365
1366pub fn find_innermost_scope(
1370 cache: &CompletionCache,
1371 byte_pos: usize,
1372 file_id: FileId,
1373) -> Option<NodeId> {
1374 cache
1376 .scope_ranges
1377 .iter()
1378 .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1379 .map(|r| r.node_id)
1380}
1381
1382pub fn resolve_name_in_scope(
1391 cache: &CompletionCache,
1392 name: &str,
1393 byte_pos: usize,
1394 file_id: FileId,
1395) -> Option<String> {
1396 let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1397
1398 loop {
1400 if let Some(decls) = cache.scope_declarations.get(¤t_scope) {
1402 for decl in decls {
1403 if decl.name == name {
1404 return Some(decl.type_id.clone());
1405 }
1406 }
1407 }
1408
1409 if let Some(bases) = cache.linearized_base_contracts.get(¤t_scope) {
1413 for &base_id in bases.iter().skip(1) {
1414 if let Some(decls) = cache.scope_declarations.get(&base_id) {
1415 for decl in decls {
1416 if decl.name == name {
1417 return Some(decl.type_id.clone());
1418 }
1419 }
1420 }
1421 }
1422 }
1423
1424 match cache.scope_parent.get(¤t_scope) {
1426 Some(&parent_id) => current_scope = parent_id,
1427 None => break, }
1429 }
1430
1431 resolve_name_to_type_id(cache, name)
1434}
1435
1436fn resolve_member_type(
1441 cache: &CompletionCache,
1442 context_type_id: &str,
1443 member_name: &str,
1444 kind: &AccessKind,
1445) -> Option<String> {
1446 let resolved_node_id = extract_node_id_from_type(context_type_id)
1447 .or_else(|| cache.type_to_node.get(context_type_id).copied())
1448 .or_else(|| {
1449 context_type_id
1451 .strip_prefix("__node_id_")
1452 .and_then(|s| s.parse::<u64>().ok())
1453 .map(NodeId)
1454 });
1455
1456 let node_id = resolved_node_id?;
1457
1458 match kind {
1459 AccessKind::Call => {
1460 cache
1462 .function_return_types
1463 .get(&(node_id, member_name.to_string()))
1464 .cloned()
1465 }
1466 AccessKind::Index => {
1467 if let Some(members) = cache.node_members.get(&node_id) {
1469 for member in members {
1470 if member.label == member_name {
1471 if let Some(tid) = cache.name_to_type.get(member_name) {
1473 if tid.starts_with("t_mapping") {
1474 return extract_mapping_value_type(tid);
1475 }
1476 return Some(tid.clone());
1477 }
1478 }
1479 }
1480 }
1481 if let Some(tid) = cache.name_to_type.get(member_name)
1483 && tid.starts_with("t_mapping")
1484 {
1485 return extract_mapping_value_type(tid);
1486 }
1487 None
1488 }
1489 AccessKind::Plain => {
1490 cache.name_to_type.get(member_name).cloned()
1492 }
1493 }
1494}
1495
1496pub struct ScopeContext {
1500 pub byte_pos: usize,
1502 pub file_id: FileId,
1504}
1505
1506fn resolve_name(
1510 cache: &CompletionCache,
1511 name: &str,
1512 scope_ctx: Option<&ScopeContext>,
1513) -> Option<String> {
1514 if let Some(ctx) = scope_ctx {
1515 resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1516 } else {
1517 resolve_name_to_type_id(cache, name)
1518 }
1519}
1520
1521pub fn get_dot_completions(
1523 cache: &CompletionCache,
1524 identifier: &str,
1525 scope_ctx: Option<&ScopeContext>,
1526) -> Vec<CompletionItem> {
1527 if let Some(items) = magic_members(identifier) {
1529 return items;
1530 }
1531
1532 let type_id = resolve_name(cache, identifier, scope_ctx);
1534
1535 if let Some(tid) = type_id {
1536 return completions_for_type(cache, &tid);
1537 }
1538
1539 vec![]
1540}
1541
1542pub fn get_chain_completions(
1545 cache: &CompletionCache,
1546 chain: &[DotSegment],
1547 scope_ctx: Option<&ScopeContext>,
1548) -> Vec<CompletionItem> {
1549 if chain.is_empty() {
1550 return vec![];
1551 }
1552
1553 if chain.len() == 1 {
1555 let seg = &chain[0];
1556
1557 match seg.kind {
1559 AccessKind::Plain => {
1560 return get_dot_completions(cache, &seg.name, scope_ctx);
1561 }
1562 AccessKind::Call => {
1563 if seg.name == "type" {
1565 return type_meta_members(seg.call_args.as_deref(), Some(cache));
1566 }
1567 if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1570 return completions_for_type(cache, &type_id);
1571 }
1572 for ((_, fn_name), ret_type) in &cache.function_return_types {
1574 if fn_name == &seg.name {
1575 return completions_for_type(cache, ret_type);
1576 }
1577 }
1578 return vec![];
1579 }
1580 AccessKind::Index => {
1581 if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1583 && tid.starts_with("t_mapping")
1584 && let Some(val_type) = extract_mapping_value_type(&tid)
1585 {
1586 return completions_for_type(cache, &val_type);
1587 }
1588 return vec![];
1589 }
1590 }
1591 }
1592
1593 let first = &chain[0];
1596 let mut current_type = match first.kind {
1597 AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1598 AccessKind::Call => {
1599 resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1601 cache
1602 .function_return_types
1603 .iter()
1604 .find(|((_, fn_name), _)| fn_name == &first.name)
1605 .map(|(_, ret_type)| ret_type.clone())
1606 })
1607 }
1608 AccessKind::Index => {
1609 resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1611 if tid.starts_with("t_mapping") {
1612 extract_mapping_value_type(&tid)
1613 } else {
1614 Some(tid)
1615 }
1616 })
1617 }
1618 };
1619
1620 for seg in &chain[1..] {
1622 let ctx_type = match ¤t_type {
1623 Some(t) => t.clone(),
1624 None => return vec![],
1625 };
1626
1627 current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1628 }
1629
1630 match current_type {
1632 Some(tid) => completions_for_type(cache, &tid),
1633 None => vec![],
1634 }
1635}
1636
1637pub fn get_static_completions() -> Vec<CompletionItem> {
1640 let mut items = Vec::new();
1641
1642 for kw in SOLIDITY_KEYWORDS {
1644 items.push(CompletionItem {
1645 label: kw.to_string(),
1646 kind: Some(CompletionItemKind::KEYWORD),
1647 ..Default::default()
1648 });
1649 }
1650
1651 for (name, detail) in MAGIC_GLOBALS {
1653 items.push(CompletionItem {
1654 label: name.to_string(),
1655 kind: Some(CompletionItemKind::VARIABLE),
1656 detail: Some(detail.to_string()),
1657 ..Default::default()
1658 });
1659 }
1660
1661 for (name, detail) in GLOBAL_FUNCTIONS {
1663 items.push(CompletionItem {
1664 label: name.to_string(),
1665 kind: Some(CompletionItemKind::FUNCTION),
1666 detail: Some(detail.to_string()),
1667 ..Default::default()
1668 });
1669 }
1670
1671 for (name, detail) in ETHER_UNITS {
1673 items.push(CompletionItem {
1674 label: name.to_string(),
1675 kind: Some(CompletionItemKind::UNIT),
1676 detail: Some(detail.to_string()),
1677 ..Default::default()
1678 });
1679 }
1680
1681 for (name, detail) in TIME_UNITS {
1683 items.push(CompletionItem {
1684 label: name.to_string(),
1685 kind: Some(CompletionItemKind::UNIT),
1686 detail: Some(detail.to_string()),
1687 ..Default::default()
1688 });
1689 }
1690
1691 items
1692}
1693
1694pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1696 let mut items = cache.names.clone();
1697 items.extend(get_static_completions());
1698 items
1699}
1700
1701pub fn append_auto_import_candidates_last(
1707 mut base: Vec<CompletionItem>,
1708 mut auto_import_candidates: Vec<CompletionItem>,
1709) -> Vec<CompletionItem> {
1710 let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1711 for item in &auto_import_candidates {
1712 let entry = unique_label_edits.entry(item.label.clone()).or_insert_with(|| {
1713 item.additional_text_edits.clone()
1714 });
1715 if *entry != item.additional_text_edits {
1716 *entry = None;
1717 }
1718 }
1719
1720 for item in &mut base {
1724 if item.additional_text_edits.is_none()
1725 && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1726 {
1727 item.additional_text_edits = Some(edits.clone());
1728 }
1729 }
1730
1731 for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1732 if item.sort_text.is_none() {
1733 item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1734 }
1735 }
1736
1737 base.extend(auto_import_candidates);
1738 base
1739}
1740
1741pub fn top_level_importable_completion_candidates(
1746 cache: &CompletionCache,
1747 current_file_path: Option<&str>,
1748 source_text: &str,
1749) -> Vec<CompletionItem> {
1750 let mut out = Vec::new();
1751 for symbols in cache.top_level_importables_by_name.values() {
1752 for symbol in symbols {
1753 if let Some(cur) = current_file_path
1754 && cur == symbol.declaring_path
1755 {
1756 continue;
1757 }
1758
1759 let import_path = match current_file_path.and_then(|cur| {
1760 to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
1761 }) {
1762 Some(p) => p,
1763 None => continue,
1764 };
1765
1766 if import_statement_already_present(source_text, &symbol.name, &import_path) {
1767 continue;
1768 }
1769
1770 let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
1771 out.push(CompletionItem {
1772 label: symbol.name.clone(),
1773 kind: Some(symbol.kind),
1774 detail: Some(format!("{} ({import_path})", symbol.node_type)),
1775 additional_text_edits: import_edit.map(|e| vec![e]),
1776 ..Default::default()
1777 });
1778 }
1779 }
1780 out
1781}
1782
1783fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
1784 let from_dir = current_file.parent()?;
1785 let rel = pathdiff::diff_paths(target_file, from_dir)?;
1786 let mut s = rel.to_string_lossy().replace('\\', "/");
1787 if !s.starts_with("./") && !s.starts_with("../") {
1788 s = format!("./{s}");
1789 }
1790 Some(s)
1791}
1792
1793fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
1794 let named = format!("import {{{symbol}}} from \"{import_path}\";");
1795 let full = format!("import \"{import_path}\";");
1796 source_text.contains(&named) || source_text.contains(&full)
1797}
1798
1799fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
1800 let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
1801 let lines: Vec<&str> = source_text.lines().collect();
1802
1803 let last_import_line = lines
1804 .iter()
1805 .enumerate()
1806 .filter(|(_, line)| line.trim_start().starts_with("import "))
1807 .map(|(idx, _)| idx)
1808 .last();
1809
1810 let insert_line = if let Some(idx) = last_import_line {
1811 idx + 1
1812 } else if let Some(idx) = lines
1813 .iter()
1814 .enumerate()
1815 .filter(|(_, line)| line.trim_start().starts_with("pragma "))
1816 .map(|(idx, _)| idx)
1817 .last()
1818 {
1819 idx + 1
1820 } else {
1821 0
1822 };
1823
1824 Some(TextEdit {
1825 range: Range {
1826 start: Position {
1827 line: insert_line as u32,
1828 character: 0,
1829 },
1830 end: Position {
1831 line: insert_line as u32,
1832 character: 0,
1833 },
1834 },
1835 new_text: import_stmt,
1836 })
1837}
1838
1839pub fn handle_completion_with_tail_candidates(
1844 cache: Option<&CompletionCache>,
1845 source_text: &str,
1846 position: Position,
1847 trigger_char: Option<&str>,
1848 file_id: Option<FileId>,
1849 tail_candidates: Vec<CompletionItem>,
1850) -> Option<CompletionResponse> {
1851 let lines: Vec<&str> = source_text.lines().collect();
1852 let line = lines.get(position.line as usize)?;
1853
1854 let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
1856 let line_start_byte: usize = source_text[..abs_byte]
1857 .rfind('\n')
1858 .map(|i| i + 1)
1859 .unwrap_or(0);
1860 let col_byte = (abs_byte - line_start_byte) as u32;
1861
1862 let scope_ctx = file_id.map(|fid| ScopeContext {
1864 byte_pos: abs_byte,
1865 file_id: fid,
1866 });
1867
1868 let items = if trigger_char == Some(".") {
1869 let chain = parse_dot_chain(line, col_byte);
1870 if chain.is_empty() {
1871 return None;
1872 }
1873 match cache {
1874 Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
1875 None => {
1876 if chain.len() == 1 {
1878 let seg = &chain[0];
1879 if seg.name == "type" && seg.kind == AccessKind::Call {
1880 type_meta_members(seg.call_args.as_deref(), None)
1882 } else if seg.kind == AccessKind::Plain {
1883 magic_members(&seg.name).unwrap_or_default()
1884 } else {
1885 vec![]
1886 }
1887 } else {
1888 vec![]
1889 }
1890 }
1891 }
1892 } else {
1893 match cache {
1894 Some(c) => {
1895 append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
1896 }
1897 None => get_static_completions(),
1898 }
1899 };
1900
1901 Some(CompletionResponse::List(CompletionList {
1902 is_incomplete: cache.is_none(),
1903 items,
1904 }))
1905}
1906
1907pub fn handle_completion(
1917 cache: Option<&CompletionCache>,
1918 source_text: &str,
1919 position: Position,
1920 trigger_char: Option<&str>,
1921 file_id: Option<FileId>,
1922) -> Option<CompletionResponse> {
1923 handle_completion_with_tail_candidates(
1924 cache,
1925 source_text,
1926 position,
1927 trigger_char,
1928 file_id,
1929 vec![],
1930 )
1931}
1932
1933const SOLIDITY_KEYWORDS: &[&str] = &[
1934 "abstract",
1935 "address",
1936 "assembly",
1937 "bool",
1938 "break",
1939 "bytes",
1940 "bytes1",
1941 "bytes4",
1942 "bytes32",
1943 "calldata",
1944 "constant",
1945 "constructor",
1946 "continue",
1947 "contract",
1948 "delete",
1949 "do",
1950 "else",
1951 "emit",
1952 "enum",
1953 "error",
1954 "event",
1955 "external",
1956 "fallback",
1957 "false",
1958 "for",
1959 "function",
1960 "if",
1961 "immutable",
1962 "import",
1963 "indexed",
1964 "int8",
1965 "int24",
1966 "int128",
1967 "int256",
1968 "interface",
1969 "internal",
1970 "library",
1971 "mapping",
1972 "memory",
1973 "modifier",
1974 "new",
1975 "override",
1976 "payable",
1977 "pragma",
1978 "private",
1979 "public",
1980 "pure",
1981 "receive",
1982 "return",
1983 "returns",
1984 "revert",
1985 "storage",
1986 "string",
1987 "struct",
1988 "true",
1989 "type",
1990 "uint8",
1991 "uint24",
1992 "uint128",
1993 "uint160",
1994 "uint256",
1995 "unchecked",
1996 "using",
1997 "view",
1998 "virtual",
1999 "while",
2000];
2001
2002const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2004
2005const TIME_UNITS: &[(&str, &str)] = &[
2007 ("seconds", "1"),
2008 ("minutes", "60 seconds"),
2009 ("hours", "3600 seconds"),
2010 ("days", "86400 seconds"),
2011 ("weeks", "604800 seconds"),
2012];
2013
2014const MAGIC_GLOBALS: &[(&str, &str)] = &[
2015 ("msg", "msg"),
2016 ("block", "block"),
2017 ("tx", "tx"),
2018 ("abi", "abi"),
2019 ("this", "address"),
2020 ("super", "contract"),
2021 ("type", "type information"),
2022];
2023
2024const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2025 ("addmod(uint256, uint256, uint256)", "uint256"),
2027 ("mulmod(uint256, uint256, uint256)", "uint256"),
2028 ("keccak256(bytes memory)", "bytes32"),
2029 ("sha256(bytes memory)", "bytes32"),
2030 ("ripemd160(bytes memory)", "bytes20"),
2031 (
2032 "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2033 "address",
2034 ),
2035 ("blockhash(uint256 blockNumber)", "bytes32"),
2037 ("blobhash(uint256 index)", "bytes32"),
2038 ("gasleft()", "uint256"),
2039 ("assert(bool condition)", ""),
2041 ("require(bool condition)", ""),
2042 ("require(bool condition, string memory message)", ""),
2043 ("revert()", ""),
2044 ("revert(string memory reason)", ""),
2045 ("selfdestruct(address payable recipient)", ""),
2047];
2048
2049#[cfg(test)]
2050mod tests {
2051 use super::{
2052 CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2053 build_completion_cache, extract_top_level_importables_for_file,
2054 };
2055 use serde_json::json;
2056 use std::collections::HashMap;
2057 use tower_lsp::lsp_types::CompletionItemKind;
2058 use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2059
2060 fn empty_cache() -> CompletionCache {
2061 CompletionCache {
2062 names: vec![],
2063 name_to_type: HashMap::new(),
2064 node_members: HashMap::new(),
2065 type_to_node: HashMap::new(),
2066 name_to_node_id: HashMap::new(),
2067 method_identifiers: HashMap::new(),
2068 function_return_types: HashMap::new(),
2069 using_for: HashMap::new(),
2070 using_for_wildcard: vec![],
2071 general_completions: vec![],
2072 scope_declarations: HashMap::new(),
2073 scope_parent: HashMap::new(),
2074 scope_ranges: vec![],
2075 path_to_file_id: HashMap::new(),
2076 linearized_base_contracts: HashMap::new(),
2077 contract_kinds: HashMap::new(),
2078 top_level_importables_by_name: HashMap::new(),
2079 top_level_importables_by_file: HashMap::new(),
2080 }
2081 }
2082
2083 #[test]
2084 fn top_level_importables_include_only_direct_declared_symbols() {
2085 let sources = json!({
2086 "/tmp/A.sol": {
2087 "id": 0,
2088 "ast": {
2089 "id": 1,
2090 "nodeType": "SourceUnit",
2091 "src": "0:100:0",
2092 "nodes": [
2093 { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2094 { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2095 { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2096 ] },
2097 { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2098 { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2099 { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2100 { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2101 { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2102 { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2103 ]
2104 }
2105 }
2106 });
2107
2108 let cache = build_completion_cache(&sources, None);
2109 let map = &cache.top_level_importables_by_name;
2110 let by_file = &cache.top_level_importables_by_file;
2111
2112 assert!(map.contains_key("C"));
2113 assert!(map.contains_key("S"));
2114 assert!(map.contains_key("E"));
2115 assert!(map.contains_key("Wad"));
2116 assert!(map.contains_key("freeFn"));
2117 assert!(map.contains_key("TOP_CONST"));
2118
2119 assert!(!map.contains_key("Alias"));
2120 assert!(!map.contains_key("inside"));
2121 assert!(!map.contains_key("TOP_VAR"));
2122
2123 let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2124 let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2125 assert!(file_names.contains(&"C"));
2126 assert!(file_names.contains(&"TOP_CONST"));
2127 assert!(!file_names.contains(&"Alias"));
2128 }
2129
2130 #[test]
2131 fn top_level_importables_keep_multiple_declarations_for_same_name() {
2132 let sources = json!({
2133 "/tmp/A.sol": {
2134 "id": 0,
2135 "ast": {
2136 "id": 1,
2137 "nodeType": "SourceUnit",
2138 "src": "0:100:0",
2139 "nodes": [
2140 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2141 ]
2142 }
2143 },
2144 "/tmp/B.sol": {
2145 "id": 1,
2146 "ast": {
2147 "id": 2,
2148 "nodeType": "SourceUnit",
2149 "src": "0:100:1",
2150 "nodes": [
2151 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2152 ]
2153 }
2154 }
2155 });
2156
2157 let cache = build_completion_cache(&sources, None);
2158 let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2159 assert_eq!(entries.len(), 2);
2160 }
2161
2162 #[test]
2163 fn extract_top_level_importables_for_file_finds_expected_symbols() {
2164 let ast = json!({
2165 "id": 1,
2166 "nodeType": "SourceUnit",
2167 "src": "0:100:0",
2168 "nodes": [
2169 { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2170 { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2171 { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2172 ]
2173 });
2174
2175 let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2176 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2177 assert!(names.contains(&"f"));
2178 assert!(names.contains(&"K"));
2179 assert!(!names.contains(&"V"));
2180 }
2181
2182 #[test]
2183 fn top_level_importables_can_be_replaced_per_file() {
2184 let sources = json!({
2185 "/tmp/A.sol": {
2186 "id": 0,
2187 "ast": {
2188 "id": 1,
2189 "nodeType": "SourceUnit",
2190 "src": "0:100:0",
2191 "nodes": [
2192 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2193 ]
2194 }
2195 },
2196 "/tmp/B.sol": {
2197 "id": 1,
2198 "ast": {
2199 "id": 2,
2200 "nodeType": "SourceUnit",
2201 "src": "0:100:1",
2202 "nodes": [
2203 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2204 ]
2205 }
2206 }
2207 });
2208
2209 let mut cache = build_completion_cache(&sources, None);
2210 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2211
2212 cache.replace_top_level_importables_for_path(
2213 "/tmp/A.sol".to_string(),
2214 vec![TopLevelImportable {
2215 name: "newA".to_string(),
2216 declaring_path: "/tmp/A.sol".to_string(),
2217 node_type: "FunctionDefinition".to_string(),
2218 kind: CompletionItemKind::FUNCTION,
2219 }],
2220 );
2221 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2222 assert!(cache.top_level_importables_by_name.contains_key("newA"));
2223
2224 cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2225 assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2226 }
2227
2228 #[test]
2229 fn append_auto_import_candidates_last_sets_tail_sort_text() {
2230 let base = vec![CompletionItem {
2231 label: "localVar".to_string(),
2232 ..Default::default()
2233 }];
2234 let auto = vec![CompletionItem {
2235 label: "ImportMe".to_string(),
2236 ..Default::default()
2237 }];
2238
2239 let out = append_auto_import_candidates_last(base, auto);
2240 assert_eq!(out.len(), 2);
2241 assert_eq!(out[1].label, "ImportMe");
2242 assert!(
2243 out[1]
2244 .sort_text
2245 .as_deref()
2246 .is_some_and(|s| s.starts_with("zz_autoimport_"))
2247 );
2248 }
2249
2250 #[test]
2251 fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2252 let base = vec![CompletionItem {
2253 label: "B".to_string(),
2254 ..Default::default()
2255 }];
2256 let auto = vec![
2257 CompletionItem {
2258 label: "B".to_string(),
2259 detail: Some("ContractDefinition (./B.sol)".to_string()),
2260 ..Default::default()
2261 },
2262 CompletionItem {
2263 label: "B".to_string(),
2264 detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2265 ..Default::default()
2266 },
2267 ];
2268
2269 let out = append_auto_import_candidates_last(base, auto);
2270 assert_eq!(out.len(), 3);
2271 }
2272
2273 #[test]
2274 fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2275 let base = vec![CompletionItem {
2276 label: "B".to_string(),
2277 ..Default::default()
2278 }];
2279 let auto = vec![CompletionItem {
2280 label: "B".to_string(),
2281 additional_text_edits: Some(vec![TextEdit {
2282 range: Range {
2283 start: Position {
2284 line: 0,
2285 character: 0,
2286 },
2287 end: Position {
2288 line: 0,
2289 character: 0,
2290 },
2291 },
2292 new_text: "import {B} from \"./B.sol\";\n".to_string(),
2293 }]),
2294 ..Default::default()
2295 }];
2296 let out = append_auto_import_candidates_last(base, auto);
2297 assert!(
2298 out[0].additional_text_edits.is_some(),
2299 "base item should inherit unique import edit"
2300 );
2301 }
2302
2303 #[test]
2304 fn top_level_importable_candidates_include_import_edit() {
2305 let mut cache = empty_cache();
2306 cache.top_level_importables_by_name.insert(
2307 "B".to_string(),
2308 vec![TopLevelImportable {
2309 name: "B".to_string(),
2310 declaring_path: "/tmp/example/B.sol".to_string(),
2311 node_type: "ContractDefinition".to_string(),
2312 kind: CompletionItemKind::CLASS,
2313 }],
2314 );
2315
2316 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2317 let items = super::top_level_importable_completion_candidates(
2318 &cache,
2319 Some("/tmp/example/A.sol"),
2320 source,
2321 );
2322 assert_eq!(items.len(), 1);
2323 let edit_text = items[0]
2324 .additional_text_edits
2325 .as_ref()
2326 .and_then(|edits| edits.first())
2327 .map(|e| e.new_text.clone())
2328 .unwrap_or_default();
2329 assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2330 }
2331
2332 #[test]
2333 fn handle_completion_general_path_keeps_base_items() {
2334 let mut cache = empty_cache();
2335 cache.general_completions = vec![CompletionItem {
2336 label: "A".to_string(),
2337 ..Default::default()
2338 }];
2339
2340 let resp = super::handle_completion(
2341 Some(&cache),
2342 "contract X {}",
2343 Position {
2344 line: 0,
2345 character: 0,
2346 },
2347 None,
2348 None,
2349 );
2350 match resp {
2351 Some(CompletionResponse::List(list)) => {
2352 assert_eq!(list.items.len(), 1);
2353 assert_eq!(list.items[0].label, "A");
2354 }
2355 _ => panic!("expected completion list"),
2356 }
2357 }
2358}