1use serde_json::Value;
2use std::collections::{HashMap, HashSet};
3use std::ops::ControlFlow;
4use std::path::Path;
5use tower_lsp::lsp_types::{
6 CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, CompletionTextEdit,
7 Position, Range, TextEdit,
8};
9
10use crate::goto::CHILD_KEYS;
11use crate::hover::build_function_signature;
12use crate::types::{FileId, NodeId, RelPath, SourceLoc, SymbolName, TypeIdentifier};
13use crate::utils::push_if_node_or_array;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TopLevelImportable {
18 pub name: String,
20 pub declaring_path: String,
22 pub node_type: String,
24 pub kind: CompletionItemKind,
26}
27
28#[derive(Debug, Clone)]
30pub struct ScopedDeclaration {
31 pub name: String,
33 pub type_id: String,
35 pub is_private: bool,
38 pub type_string: Option<String>,
40 pub declared_at: Option<usize>,
44}
45
46#[derive(Debug, Clone)]
48pub struct ScopeRange {
49 pub node_id: NodeId,
51 pub start: usize,
53 pub end: usize,
55 pub file_id: FileId,
57}
58
59#[derive(Debug)]
61pub struct CompletionCache {
62 pub names: Vec<CompletionItem>,
66
67 pub name_to_type: HashMap<SymbolName, TypeIdentifier>,
69
70 pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
72
73 pub type_to_node: HashMap<TypeIdentifier, NodeId>,
75
76 pub name_to_node_id: HashMap<SymbolName, NodeId>,
78
79 pub nested_types: HashMap<(NodeId, SymbolName), NodeId>,
83
84 pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
87
88 pub function_return_types: HashMap<(NodeId, String), String>,
91
92 pub using_for: HashMap<TypeIdentifier, Vec<CompletionItem>>,
95
96 pub using_for_wildcard: Vec<CompletionItem>,
98
99 pub general_completions: Vec<CompletionItem>,
102
103 pub general_labels: HashSet<String>,
106
107 pub unscoped_variables: Vec<CompletionItem>,
110
111 pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
115
116 pub scope_parent: HashMap<NodeId, NodeId>,
119
120 pub scope_ranges: Vec<ScopeRange>,
123
124 pub path_to_file_id: HashMap<RelPath, FileId>,
127
128 pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
132
133 pub contract_kinds: HashMap<NodeId, String>,
137
138 pub type_name_members: HashMap<SymbolName, Vec<CompletionItem>>,
142
143 pub top_level_importables_by_name: HashMap<SymbolName, Vec<TopLevelImportable>>,
149
150 pub top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>>,
155}
156
157fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
159 match node_type {
160 "FunctionDefinition" => CompletionItemKind::FUNCTION,
161 "VariableDeclaration" => CompletionItemKind::VARIABLE,
162 "ContractDefinition" => CompletionItemKind::CLASS,
163 "StructDefinition" => CompletionItemKind::STRUCT,
164 "EnumDefinition" => CompletionItemKind::ENUM,
165 "EnumValue" => CompletionItemKind::ENUM_MEMBER,
166 "EventDefinition" => CompletionItemKind::EVENT,
167 "ErrorDefinition" => CompletionItemKind::EVENT,
168 "ModifierDefinition" => CompletionItemKind::METHOD,
169 "ImportDirective" => CompletionItemKind::MODULE,
170 _ => CompletionItemKind::TEXT,
171 }
172}
173
174fn parse_src(node: &Value) -> Option<SourceLoc> {
177 let src = node.get("src").and_then(|v| v.as_str())?;
178 SourceLoc::parse(src)
179}
180
181pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
186 let mut last_id = None;
189 let mut i = 0;
190 let bytes = type_id.as_bytes();
191 while i < bytes.len() {
192 if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
193 i += 2;
194 let start = i;
195 while i < bytes.len() && bytes[i].is_ascii_digit() {
196 i += 1;
197 }
198 if i > start
199 && let Ok(id) = type_id[start..i].parse::<i64>()
200 {
201 last_id = Some(NodeId(id));
202 }
203 } else {
204 i += 1;
205 }
206 }
207 last_id
208}
209
210pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
217 let mut current = type_id;
218
219 loop {
220 if !current.starts_with("t_mapping$_") {
221 let result = current.trim_end_matches("_$");
224 return if result.is_empty() {
225 None
226 } else {
227 Some(result.to_string())
228 };
229 }
230
231 let inner = ¤t["t_mapping$_".len()..];
233
234 let mut depth = 0i32;
238 let bytes = inner.as_bytes();
239 let mut split_pos = None;
240
241 let mut i = 0;
242 while i < bytes.len() {
243 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
244 depth += 1;
245 i += 2;
246 } else if i + 2 < bytes.len()
247 && bytes[i] == b'_'
248 && bytes[i + 1] == b'$'
249 && bytes[i + 2] == b'_'
250 && depth == 0
251 {
252 split_pos = Some(i);
254 break;
255 } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
256 depth -= 1;
257 i += 2;
258 } else {
259 i += 1;
260 }
261 }
262
263 if let Some(pos) = split_pos {
264 current = &inner[pos + 3..];
266 } else {
267 return None;
268 }
269 }
270}
271
272fn count_abi_params(signature: &str) -> usize {
275 let start = match signature.find('(') {
277 Some(i) => i + 1,
278 None => return 0,
279 };
280 let bytes = signature.as_bytes();
281 if start >= bytes.len() {
282 return 0;
283 }
284 if bytes[start] == b')' {
286 return 0;
287 }
288 let mut count = 1; let mut depth = 0;
290 for &b in &bytes[start..] {
291 match b {
292 b'(' => depth += 1,
293 b')' => {
294 if depth == 0 {
295 break;
296 }
297 depth -= 1;
298 }
299 b',' if depth == 0 => count += 1,
300 _ => {}
301 }
302 }
303 count
304}
305
306fn count_signature_params(sig: &str) -> usize {
308 count_abi_params(sig)
309}
310
311fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
312 match node_type {
313 "ContractDefinition"
314 | "StructDefinition"
315 | "EnumDefinition"
316 | "UserDefinedValueTypeDefinition"
317 | "FunctionDefinition" => true,
318 "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
319 _ => false,
320 }
321}
322
323fn build_top_level_importables_by_name(
324 by_file: &HashMap<RelPath, Vec<TopLevelImportable>>,
325) -> HashMap<SymbolName, Vec<TopLevelImportable>> {
326 let mut by_name: HashMap<SymbolName, Vec<TopLevelImportable>> = HashMap::new();
327 for symbols in by_file.values() {
328 for symbol in symbols {
329 by_name
330 .entry(SymbolName::new(symbol.name.clone()))
331 .or_default()
332 .push(symbol.clone());
333 }
334 }
335 by_name
336}
337
338pub fn extract_top_level_importables_for_file(path: &str, ast: &Value) -> Vec<TopLevelImportable> {
343 let mut out: Vec<TopLevelImportable> = Vec::new();
344 let mut stack: Vec<&Value> = vec![ast];
345 let mut source_unit_id: Option<NodeId> = None;
346
347 while let Some(tree) = stack.pop() {
348 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
349 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
350 if node_type == "SourceUnit" {
351 source_unit_id = node_id;
352 }
353 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
354
355 if !name.is_empty()
356 && is_top_level_importable_decl(node_type, tree)
357 && let Some(src_scope) = source_unit_id
358 && tree.get("scope").and_then(|v| v.as_i64()) == Some(src_scope.0)
359 {
360 out.push(TopLevelImportable {
361 name: name.to_string(),
362 declaring_path: path.to_string(),
363 node_type: node_type.to_string(),
364 kind: node_type_to_completion_kind(node_type),
365 });
366 }
367
368 for key in CHILD_KEYS {
369 push_if_node_or_array(tree, key, &mut stack);
370 }
371 }
372
373 out
374}
375
376impl CompletionCache {
377 pub fn replace_top_level_importables_for_path(
380 &mut self,
381 path: String,
382 symbols: Vec<TopLevelImportable>,
383 ) {
384 self.top_level_importables_by_file
385 .insert(RelPath::new(path), symbols);
386 self.top_level_importables_by_name =
387 build_top_level_importables_by_name(&self.top_level_importables_by_file);
388 }
389}
390
391pub fn build_completion_cache(
399 sources: &Value,
400 contracts: Option<&Value>,
401 file_id_remap: Option<&HashMap<u64, FileId>>,
402) -> CompletionCache {
403 let source_count = sources.as_object().map_or(0, |obj| obj.len());
404 let est_names = source_count * 20;
407 let est_contracts = source_count * 5;
408
409 let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
410 let mut seen_names: HashMap<SymbolName, usize> = HashMap::with_capacity(est_names);
411 let mut name_to_type: HashMap<SymbolName, TypeIdentifier> = HashMap::with_capacity(est_names);
412 let mut nested_types: HashMap<(NodeId, SymbolName), NodeId> = HashMap::new();
413 let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
414 HashMap::with_capacity(est_contracts);
415 let mut type_to_node: HashMap<TypeIdentifier, NodeId> = HashMap::with_capacity(est_contracts);
416 let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
417 HashMap::with_capacity(est_contracts);
418 let mut name_to_node_id: HashMap<SymbolName, NodeId> = HashMap::with_capacity(est_names);
419 let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
420 let mut type_name_members: HashMap<SymbolName, Vec<CompletionItem>> = HashMap::new();
421
422 let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
424
425 let mut function_signatures: HashMap<NodeId, HashMap<SymbolName, Vec<String>>> =
427 HashMap::with_capacity(est_contracts);
428
429 let mut function_return_types: HashMap<(NodeId, String), String> =
431 HashMap::with_capacity(source_count * 10);
432
433 let mut using_for: HashMap<TypeIdentifier, Vec<CompletionItem>> =
435 HashMap::with_capacity(source_count);
436 let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
437
438 let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
440
441 let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
443 HashMap::with_capacity(est_contracts);
444 let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
445 let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
446 let mut path_to_file_id: HashMap<RelPath, FileId> = HashMap::with_capacity(source_count);
447 let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
448 HashMap::with_capacity(est_contracts);
449 let mut top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>> =
450 HashMap::with_capacity(est_names);
451 let mut var_scope_kinds: HashMap<NodeId, &str> = HashMap::new();
457 let mut unscoped_variables: Vec<CompletionItem> = Vec::new();
458 let mut seen_unscoped: HashSet<String> = HashSet::new();
459
460 if let Some(sources_obj) = sources.as_object() {
461 for (path, source_data) in sources_obj {
462 if let Some(ast) = source_data.get("ast") {
463 if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
467 let canonical_fid = file_id_remap
468 .and_then(|r| r.get(&fid).copied())
469 .unwrap_or(FileId(fid));
470 path_to_file_id.insert(RelPath::new(path), canonical_fid);
471 }
472 let file_importables = extract_top_level_importables_for_file(path, ast);
473 if !file_importables.is_empty() {
474 top_level_importables_by_file.insert(RelPath::new(path), file_importables);
475 }
476 let mut stack: Vec<&Value> = vec![ast];
477
478 while let Some(tree) = stack.pop() {
479 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
480 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
481 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
482
483 let is_scope_node = matches!(
491 node_type,
492 "SourceUnit"
493 | "ContractDefinition"
494 | "FunctionDefinition"
495 | "ModifierDefinition"
496 | "Block"
497 | "UncheckedBlock"
498 | "ForStatement"
499 | "TryCatchClause"
500 );
501 if is_scope_node && let Some(nid) = node_id {
502 if let Some(src_loc) = parse_src(tree) {
503 let canonical_fid = file_id_remap
504 .and_then(|r| r.get(&src_loc.file_id.0).copied())
505 .unwrap_or(src_loc.file_id);
506 scope_ranges.push(ScopeRange {
507 node_id: nid,
508 start: src_loc.offset,
509 end: src_loc.end(),
510 file_id: canonical_fid,
511 });
512 }
513 if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_i64()) {
515 scope_parent.insert(nid, NodeId(parent_id));
516 }
517 }
518
519 if node_type == "ContractDefinition"
521 && let Some(nid) = node_id
522 && let Some(bases) = tree
523 .get("linearizedBaseContracts")
524 .and_then(|v| v.as_array())
525 {
526 let base_ids: Vec<NodeId> = bases
527 .iter()
528 .filter_map(|b| b.as_i64())
529 .map(NodeId)
530 .collect();
531 if !base_ids.is_empty() {
532 linearized_base_contracts.insert(nid, base_ids);
533 }
534 }
535
536 if node_type == "VariableDeclaration"
538 && !name.is_empty()
539 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
540 && let Some(tid) = tree
541 .get("typeDescriptions")
542 .and_then(|td| td.get("typeIdentifier"))
543 .and_then(|v| v.as_str())
544 {
545 scope_declarations
546 .entry(NodeId(scope_raw))
547 .or_default()
548 .push(ScopedDeclaration {
549 name: name.to_string(),
550 type_id: tid.to_string(),
551 is_private: tree.get("visibility").and_then(|v| v.as_str())
552 == Some("private"),
553 type_string: type_string_of(tree),
554 declared_at: var_scope_kinds
555 .get(&NodeId(scope_raw))
556 .filter(|k| is_statement_scope(k))
557 .and(parse_src(tree))
558 .map(|loc| loc.offset),
559 });
560 }
561
562 if node_type == "FunctionDefinition"
564 && !name.is_empty()
565 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
566 && let Some(tid) = tree
567 .get("typeDescriptions")
568 .and_then(|td| td.get("typeIdentifier"))
569 .and_then(|v| v.as_str())
570 {
571 scope_declarations
572 .entry(NodeId(scope_raw))
573 .or_default()
574 .push(ScopedDeclaration {
575 name: name.to_string(),
576 type_id: tid.to_string(),
577 is_private: tree.get("visibility").and_then(|v| v.as_str())
578 == Some("private"),
579 type_string: type_string_of(tree),
580 declared_at: None,
581 });
582 }
583
584 if (matches!(
585 node_type,
586 "SourceUnit"
587 | "ContractDefinition"
588 | "FunctionDefinition"
589 | "ModifierDefinition"
590 ) || is_statement_scope(node_type))
591 && let Some(id) = node_id
592 {
593 var_scope_kinds.insert(id, node_type);
594 }
595
596 let var_scope_kind = if node_type == "VariableDeclaration" {
602 tree.get("scope")
603 .and_then(|v| v.as_i64())
604 .map(|scope| var_scope_kinds.get(&NodeId(scope)).copied())
605 } else {
606 None
607 };
608 let is_hidden_variable = var_scope_kind.is_some_and(|kind| {
609 !matches!(kind, Some("SourceUnit" | "ContractDefinition"))
610 });
611
612 if !name.is_empty()
615 && var_scope_kind.flatten().is_some_and(|kind| {
616 !matches!(kind, "SourceUnit" | "ContractDefinition")
617 })
618 && seen_unscoped.insert(name.to_string())
619 {
620 unscoped_variables.push(CompletionItem {
621 label: name.to_string(),
622 kind: Some(CompletionItemKind::VARIABLE),
623 detail: type_string_of(tree),
624 ..Default::default()
625 });
626 }
627 let is_bare_name_decl = matches!(
628 node_type,
629 "ContractDefinition"
630 | "StructDefinition"
631 | "EnumDefinition"
632 | "UserDefinedValueTypeDefinition"
633 | "FunctionDefinition"
634 | "ModifierDefinition"
635 | "EventDefinition"
636 | "ErrorDefinition"
637 | "VariableDeclaration"
638 | "ImportDirective"
639 );
640
641 if !name.is_empty()
643 && is_bare_name_decl
644 && !is_hidden_variable
645 && !seen_names.contains_key(name)
646 {
647 let type_string = tree
648 .get("typeDescriptions")
649 .and_then(|td| td.get("typeString"))
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string());
652
653 let kind = node_type_to_completion_kind(node_type);
654
655 let item = CompletionItem {
656 label: name.to_string(),
657 kind: Some(kind),
658 detail: type_string,
659 ..Default::default()
660 };
661
662 let idx = names.len();
663 names.push(item);
664 seen_names.insert(SymbolName::new(name), idx);
665 }
666
667 if !name.is_empty()
675 && !name_to_type.contains_key(name)
676 && let Some(tid) = tree
677 .get("typeDescriptions")
678 .and_then(|td| td.get("typeIdentifier"))
679 .and_then(|v| v.as_str())
680 && !tid.starts_with("t_type$")
681 {
682 name_to_type.insert(SymbolName::new(name), TypeIdentifier::new(tid));
683 }
684
685 if node_type == "ImportDirective" {
688 let symbol_aliases = tree
689 .get("symbolAliases")
690 .and_then(|v| v.as_array())
691 .into_iter()
692 .flatten()
693 .filter_map(|a| a.get("local").and_then(|v| v.as_str()));
694 let unit_alias = tree.get("unitAlias").and_then(|v| v.as_str());
695 for alias in symbol_aliases.chain(unit_alias) {
696 if alias.is_empty() || seen_names.contains_key(alias) {
697 continue;
698 }
699 seen_names.insert(SymbolName::new(alias), names.len());
700 names.push(CompletionItem {
701 label: alias.to_string(),
702 kind: Some(CompletionItemKind::MODULE),
703 ..Default::default()
704 });
705 }
706 }
707
708 if node_type == "StructDefinition"
710 && let Some(id) = node_id
711 {
712 let mut members = Vec::new();
713 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
714 for member in member_array {
715 let member_name =
716 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
717 if member_name.is_empty() {
718 continue;
719 }
720 let member_type = member
721 .get("typeDescriptions")
722 .and_then(|td| td.get("typeString"))
723 .and_then(|v| v.as_str())
724 .map(|s| s.to_string());
725
726 members.push(CompletionItem {
727 label: member_name.to_string(),
728 kind: Some(CompletionItemKind::FIELD),
729 detail: member_type,
730 ..Default::default()
731 });
732 }
733 }
734 if !members.is_empty() {
735 node_members.insert(id, members);
736 }
737
738 if let Some(tid) = tree
740 .get("typeDescriptions")
741 .and_then(|td| td.get("typeIdentifier"))
742 .and_then(|v| v.as_str())
743 {
744 type_to_node.insert(TypeIdentifier::new(tid), id);
745 }
746 }
747
748 if node_type == "ContractDefinition"
750 && let Some(id) = node_id
751 {
752 let mut members = Vec::new();
753 let mut fn_sigs: HashMap<SymbolName, Vec<String>> = HashMap::new();
754 if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
755 for member in nodes_array {
756 let member_type = member
757 .get("nodeType")
758 .and_then(|v| v.as_str())
759 .unwrap_or("");
760 let member_name =
761 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
762 if member_name.is_empty() {
763 continue;
764 }
765
766 if member_type == "EnumDefinition"
767 && let Some(member_id) =
768 member.get("id").and_then(|v| v.as_i64())
769 {
770 nested_types.insert(
771 (id, SymbolName::new(member_name)),
772 NodeId(member_id),
773 );
774 }
775
776 let (member_detail, label_details) =
778 if member_type == "FunctionDefinition" {
779 if let Some(ret_params) = member
783 .get("returnParameters")
784 .and_then(|rp| rp.get("parameters"))
785 .and_then(|v| v.as_array())
786 && ret_params.len() == 1
787 && let Some(ret_tid) = ret_params[0]
788 .get("typeDescriptions")
789 .and_then(|td| td.get("typeIdentifier"))
790 .and_then(|v| v.as_str())
791 {
792 function_return_types.insert(
793 (id, member_name.to_string()),
794 ret_tid.to_string(),
795 );
796 }
797
798 if let Some(sig) = build_function_signature(member) {
799 fn_sigs
800 .entry(SymbolName::new(member_name))
801 .or_default()
802 .push(sig.clone());
803 (Some(sig), None)
804 } else {
805 (
806 member
807 .get("typeDescriptions")
808 .and_then(|td| td.get("typeString"))
809 .and_then(|v| v.as_str())
810 .map(|s| s.to_string()),
811 None,
812 )
813 }
814 } else {
815 (
816 member
817 .get("typeDescriptions")
818 .and_then(|td| td.get("typeString"))
819 .and_then(|v| v.as_str())
820 .map(|s| s.to_string()),
821 None,
822 )
823 };
824
825 let kind = node_type_to_completion_kind(member_type);
826 members.push(CompletionItem {
827 label: member_name.to_string(),
828 kind: Some(kind),
829 detail: member_detail,
830 label_details,
831 ..Default::default()
832 });
833 }
834 }
835 if !members.is_empty() {
836 node_members.insert(id, members);
837 }
838 if !fn_sigs.is_empty() {
839 function_signatures.insert(id, fn_sigs);
840 }
841
842 if let Some(tid) = tree
843 .get("typeDescriptions")
844 .and_then(|td| td.get("typeIdentifier"))
845 .and_then(|v| v.as_str())
846 {
847 type_to_node.insert(TypeIdentifier::new(tid), id);
848 }
849
850 if !name.is_empty() {
852 contract_locations.push((path.clone(), name.to_string(), id));
853 name_to_node_id.insert(SymbolName::new(name), id);
854 }
855
856 if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
858 contract_kinds.insert(id, ck.to_string());
859 }
860 }
861
862 if node_type == "EnumDefinition"
864 && let Some(id) = node_id
865 {
866 let mut members = Vec::new();
867 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
868 for member in member_array {
869 let member_name =
870 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
871 if member_name.is_empty() {
872 continue;
873 }
874 members.push(CompletionItem {
875 label: member_name.to_string(),
876 kind: Some(CompletionItemKind::ENUM_MEMBER),
877 detail: None,
878 ..Default::default()
879 });
880 }
881 }
882 if !members.is_empty() {
883 node_members.insert(id, members);
884 }
885
886 if let Some(tid) = tree
887 .get("typeDescriptions")
888 .and_then(|td| td.get("typeIdentifier"))
889 .and_then(|v| v.as_str())
890 {
891 type_to_node.insert(TypeIdentifier::new(tid), id);
892 }
893 }
894
895 if node_type == "UserDefinedValueTypeDefinition" && !name.is_empty() {
897 let underlying = tree
898 .get("underlyingType")
899 .and_then(|t| t.get("typeDescriptions"))
900 .and_then(|td| td.get("typeString"))
901 .and_then(|v| v.as_str())
902 .unwrap_or("");
903 type_name_members.insert(
904 SymbolName::new(name),
905 vec![
906 builtin_member(
907 &format!("wrap({underlying})"),
908 CompletionItemKind::FUNCTION,
909 Some(name),
910 ),
911 builtin_member(
912 &format!("unwrap({name})"),
913 CompletionItemKind::FUNCTION,
914 Some(underlying),
915 ),
916 ],
917 );
918 }
919
920 if node_type == "UsingForDirective" {
922 let target_type = tree.get("typeName").and_then(|tn| {
924 tn.get("typeDescriptions")
925 .and_then(|td| td.get("typeIdentifier"))
926 .and_then(|v| v.as_str())
927 .map(|s| s.to_string())
928 });
929
930 if let Some(lib) = tree.get("libraryName") {
932 if let Some(lib_id) =
933 lib.get("referencedDeclaration").and_then(|v| v.as_i64())
934 {
935 using_for_directives.push((NodeId(lib_id), target_type));
936 }
937 }
938 else if let Some(func_list) =
942 tree.get("functionList").and_then(|v| v.as_array())
943 {
944 for entry in func_list {
945 if entry.get("operator").is_some() {
947 continue;
948 }
949 if let Some(def) = entry.get("definition") {
950 let fn_name =
951 def.get("name").and_then(|v| v.as_str()).unwrap_or("");
952 if !fn_name.is_empty() {
953 let items = if let Some(ref tid) = target_type {
954 using_for
955 .entry(TypeIdentifier::new(tid.clone()))
956 .or_default()
957 } else {
958 &mut using_for_wildcard
959 };
960 items.push(CompletionItem {
961 label: fn_name.to_string(),
962 kind: Some(CompletionItemKind::FUNCTION),
963 detail: None,
964 ..Default::default()
965 });
966 }
967 }
968 }
969 }
970 }
971
972 for key in CHILD_KEYS {
974 push_if_node_or_array(tree, key, &mut stack);
975 }
976 }
977 }
978 }
979 }
980
981 for (lib_id, target_type) in &using_for_directives {
984 if let Some(lib_members) = node_members.get(lib_id) {
985 let items: Vec<CompletionItem> = lib_members
986 .iter()
987 .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
988 .cloned()
989 .collect();
990 if !items.is_empty() {
991 if let Some(tid) = target_type {
992 using_for
993 .entry(TypeIdentifier::new(tid.clone()))
994 .or_default()
995 .extend(items);
996 } else {
997 using_for_wildcard.extend(items);
998 }
999 }
1000 }
1001 }
1002
1003 if let Some(contracts_val) = contracts
1005 && let Some(contracts_obj) = contracts_val.as_object()
1006 {
1007 for (path, contract_name, node_id) in &contract_locations {
1008 let fn_sigs = function_signatures.get(node_id);
1010
1011 if let Some(path_entry) = contracts_obj.get(path)
1012 && let Some(contract_entry) = path_entry.get(contract_name)
1013 && let Some(evm) = contract_entry.get("evm")
1014 && let Some(methods) = evm.get("methodIdentifiers")
1015 && let Some(methods_obj) = methods.as_object()
1016 {
1017 let mut items: Vec<CompletionItem> = Vec::new();
1018 for (signature, selector_val) in methods_obj {
1019 let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
1022 let selector_str = selector_val
1023 .as_str()
1024 .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
1025 .unwrap_or_default();
1026
1027 let description =
1029 fn_sigs
1030 .and_then(|sigs| sigs.get(&fn_name))
1031 .and_then(|sig_list| {
1032 if sig_list.len() == 1 {
1033 Some(sig_list[0].clone())
1035 } else {
1036 let abi_param_count = count_abi_params(signature);
1038 sig_list
1039 .iter()
1040 .find(|s| count_signature_params(s) == abi_param_count)
1041 .cloned()
1042 }
1043 });
1044
1045 items.push(CompletionItem {
1049 label: fn_name,
1050 kind: Some(CompletionItemKind::FUNCTION),
1051 detail: Some(signature.clone()),
1052 label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
1053 detail: None,
1054 description,
1055 }),
1056 documentation: (!selector_str.is_empty()).then(|| {
1057 tower_lsp::lsp_types::Documentation::String(format!(
1058 "selector: {selector_str}"
1059 ))
1060 }),
1061 ..Default::default()
1062 });
1063 }
1064 if !items.is_empty() {
1065 method_identifiers.insert(*node_id, items);
1066 }
1067 }
1068 }
1069 }
1070
1071 let mut general_completions = names.clone();
1073 general_completions.extend(get_static_completions());
1074 let general_labels: HashSet<String> = general_completions
1075 .iter()
1076 .map(|item| item.label.clone())
1077 .collect();
1078
1079 scope_ranges.sort_by_key(|r| r.end - r.start);
1081
1082 let orphan_ids: Vec<NodeId> = scope_ranges
1088 .iter()
1089 .filter(|r| !scope_parent.contains_key(&r.node_id))
1090 .map(|r| r.node_id)
1091 .collect();
1092 let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
1094 .iter()
1095 .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
1096 .collect();
1097 for orphan_id in &orphan_ids {
1098 if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
1099 let parent = scope_ranges
1102 .iter()
1103 .find(|r| {
1104 r.node_id != *orphan_id
1105 && r.file_id == file_id
1106 && r.start <= start
1107 && r.end >= end
1108 && (r.end - r.start) > (end - start)
1109 })
1110 .map(|r| r.node_id);
1111 if let Some(parent_id) = parent {
1112 scope_parent.insert(*orphan_id, parent_id);
1113 }
1114 }
1115 }
1116
1117 let top_level_importables_by_name =
1118 build_top_level_importables_by_name(&top_level_importables_by_file);
1119
1120 CompletionCache {
1121 names,
1122 name_to_type,
1123 nested_types,
1124 node_members,
1125 type_to_node,
1126 name_to_node_id,
1127 method_identifiers,
1128 function_return_types,
1129 using_for,
1130 using_for_wildcard,
1131 general_completions,
1132 general_labels,
1133 unscoped_variables,
1134 scope_declarations,
1135 scope_parent,
1136 scope_ranges,
1137 path_to_file_id,
1138 linearized_base_contracts,
1139 contract_kinds,
1140 type_name_members,
1141 top_level_importables_by_name,
1142 top_level_importables_by_file,
1143 }
1144}
1145
1146fn is_statement_scope(node_type: &str) -> bool {
1148 matches!(
1149 node_type,
1150 "Block" | "UncheckedBlock" | "ForStatement" | "TryCatchClause"
1151 )
1152}
1153
1154fn type_string_of(node: &Value) -> Option<String> {
1155 node.get("typeDescriptions")
1156 .and_then(|td| td.get("typeString"))
1157 .and_then(|v| v.as_str())
1158 .map(str::to_string)
1159}
1160
1161fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
1163 let items = match name {
1164 "msg" => vec![
1165 ("data", "bytes calldata"),
1166 ("sender", "address"),
1167 ("sig", "bytes4"),
1168 ("value", "uint256"),
1169 ],
1170 "block" => vec![
1171 ("basefee", "uint256"),
1172 ("blobbasefee", "uint256"),
1173 ("chainid", "uint256"),
1174 ("coinbase", "address payable"),
1175 ("difficulty", "uint256"),
1176 ("gaslimit", "uint256"),
1177 ("number", "uint256"),
1178 ("prevrandao", "uint256"),
1179 ("timestamp", "uint256"),
1180 ],
1181 "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
1182 "abi" => vec![
1183 ("decode(bytes memory, (...))", "..."),
1184 ("encode(...)", "bytes memory"),
1185 ("encodePacked(...)", "bytes memory"),
1186 ("encodeWithSelector(bytes4, ...)", "bytes memory"),
1187 ("encodeWithSignature(string memory, ...)", "bytes memory"),
1188 ("encodeCall(function, (...))", "bytes memory"),
1189 ],
1190 "type" => vec![
1193 ("name", "string"),
1194 ("creationCode", "bytes memory"),
1195 ("runtimeCode", "bytes memory"),
1196 ("interfaceId", "bytes4"),
1197 ("min", "T"),
1198 ("max", "T"),
1199 ],
1200 "bytes" => vec![("concat(...)", "bytes memory")],
1202 "string" => vec![("concat(...)", "string memory")],
1203 _ => return None,
1204 };
1205
1206 Some(
1207 items
1208 .into_iter()
1209 .map(|(signature, detail)| {
1210 builtin_member(signature, CompletionItemKind::PROPERTY, Some(detail))
1211 })
1212 .collect(),
1213 )
1214}
1215
1216fn builtin_member(
1221 signature: &str,
1222 kind: CompletionItemKind,
1223 detail: Option<&str>,
1224) -> CompletionItem {
1225 let (name, params) = match signature.find('(') {
1226 Some(i) => (&signature[..i], Some(signature[i..].to_string())),
1227 None => (signature, None),
1228 };
1229 CompletionItem {
1230 label: name.to_string(),
1231 kind: Some(kind),
1232 detail: detail.filter(|d| !d.is_empty()).map(str::to_string),
1233 label_details: params.map(|p| tower_lsp::lsp_types::CompletionItemLabelDetails {
1234 detail: Some(p),
1235 description: None,
1236 }),
1237 ..Default::default()
1238 }
1239}
1240
1241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1244enum TypeMetaKind {
1245 Contract,
1247 Interface,
1249 IntegerType,
1251 Unknown,
1253}
1254
1255fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
1257 if arg == "int" || arg == "uint" {
1259 return TypeMetaKind::IntegerType;
1260 }
1261 if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1262 && let Ok(n) = suffix.parse::<u16>()
1263 && (8..=256).contains(&n)
1264 && n % 8 == 0
1265 {
1266 return TypeMetaKind::IntegerType;
1267 }
1268
1269 if let Some(c) = cache
1271 && let Some(&node_id) = c.name_to_node_id.get(arg)
1272 {
1273 return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1274 Some("interface") => TypeMetaKind::Interface,
1275 Some("library") => TypeMetaKind::Contract, _ => TypeMetaKind::Contract,
1277 };
1278 }
1279
1280 TypeMetaKind::Unknown
1281}
1282
1283fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1285 let kind = match arg {
1286 Some(a) => classify_type_arg(a, cache),
1287 None => TypeMetaKind::Unknown,
1288 };
1289
1290 let items: Vec<(&str, &str)> = match kind {
1291 TypeMetaKind::Contract => vec![
1292 ("name", "string"),
1293 ("creationCode", "bytes memory"),
1294 ("runtimeCode", "bytes memory"),
1295 ],
1296 TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1297 TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1298 TypeMetaKind::Unknown => vec![
1299 ("name", "string"),
1300 ("creationCode", "bytes memory"),
1301 ("runtimeCode", "bytes memory"),
1302 ("interfaceId", "bytes4"),
1303 ("min", "T"),
1304 ("max", "T"),
1305 ],
1306 };
1307
1308 items
1309 .into_iter()
1310 .map(|(label, detail)| CompletionItem {
1311 label: label.to_string(),
1312 kind: Some(CompletionItemKind::PROPERTY),
1313 detail: Some(detail.to_string()),
1314 ..Default::default()
1315 })
1316 .collect()
1317}
1318
1319fn address_members() -> Vec<CompletionItem> {
1321 [
1322 ("balance", "uint256", CompletionItemKind::PROPERTY),
1323 ("code", "bytes memory", CompletionItemKind::PROPERTY),
1324 ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1325 ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1326 ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1327 (
1328 "call(bytes memory)",
1329 "(bool, bytes memory)",
1330 CompletionItemKind::FUNCTION,
1331 ),
1332 (
1333 "delegatecall(bytes memory)",
1334 "(bool, bytes memory)",
1335 CompletionItemKind::FUNCTION,
1336 ),
1337 (
1338 "staticcall(bytes memory)",
1339 "(bool, bytes memory)",
1340 CompletionItemKind::FUNCTION,
1341 ),
1342 ]
1343 .iter()
1344 .map(|(signature, detail, kind)| builtin_member(signature, *kind, Some(detail)))
1345 .collect()
1346}
1347
1348#[derive(Debug, Clone, PartialEq)]
1350pub enum AccessKind {
1351 Plain,
1353 Call,
1355 Index,
1357}
1358
1359#[derive(Debug, Clone, PartialEq)]
1361pub struct DotSegment {
1362 pub name: String,
1363 pub kind: AccessKind,
1364 pub call_args: Option<String>,
1367}
1368
1369fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1373 let close = bytes[pos];
1374 let open = match close {
1375 b')' => b'(',
1376 b']' => b'[',
1377 _ => return pos,
1378 };
1379 let mut depth = 1u32;
1380 let mut i = pos;
1381 while i > 0 && depth > 0 {
1382 i -= 1;
1383 if bytes[i] == close {
1384 depth += 1;
1385 } else if bytes[i] == open {
1386 depth -= 1;
1387 }
1388 }
1389 i
1390}
1391
1392pub fn is_member_completion(line: &str, character: u32) -> bool {
1402 let bytes = line.as_bytes();
1403 let mut pos = (character as usize).min(bytes.len());
1404 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1405 pos -= 1;
1406 }
1407 pos > 0 && bytes[pos - 1] == b'.'
1408}
1409
1410pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1411 let col = character as usize;
1412 if col == 0 {
1413 return vec![];
1414 }
1415
1416 let bytes = line.as_bytes();
1417 let mut segments: Vec<DotSegment> = Vec::new();
1418
1419 let mut pos = col.min(bytes.len());
1422 if is_member_completion(line, character) {
1423 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1424 pos -= 1;
1425 }
1426 }
1427 if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1428 pos -= 1;
1429 }
1430
1431 loop {
1432 if pos == 0 {
1433 break;
1434 }
1435
1436 let (kind, call_args) = if bytes[pos - 1] == b')' {
1438 let close = pos - 1; pos = skip_brackets_backwards(bytes, close);
1440 let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1442 .trim()
1443 .to_string();
1444 let args = if args_text.is_empty() {
1445 None
1446 } else {
1447 Some(args_text)
1448 };
1449 (AccessKind::Call, args)
1450 } else if bytes[pos - 1] == b']' {
1451 pos -= 1; pos = skip_brackets_backwards(bytes, pos);
1453 (AccessKind::Index, None)
1454 } else {
1455 (AccessKind::Plain, None)
1456 };
1457
1458 let end = pos;
1460 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1461 pos -= 1;
1462 }
1463
1464 if pos == end {
1465 break;
1467 }
1468
1469 let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1470 segments.push(DotSegment {
1471 name,
1472 kind,
1473 call_args,
1474 });
1475
1476 if pos > 0 && bytes[pos - 1] == b'.' {
1478 pos -= 1; } else {
1480 break;
1481 }
1482 }
1483
1484 segments.reverse(); segments
1486}
1487
1488pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1491 let segments = parse_dot_chain(line, character);
1492 segments.last().map(|s| s.name.clone())
1493}
1494
1495#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1496Solidity AST uses different suffixes in different contexts:
1497 - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1498 - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1499 - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1500All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1501fn strip_type_suffix(type_id: &str) -> &str {
1502 let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1503 s.strip_suffix("_storage")
1504 .or_else(|| s.strip_suffix("_memory"))
1505 .or_else(|| s.strip_suffix("_calldata"))
1506 .unwrap_or(s)
1507}
1508
1509fn lookup_using_for(
1513 cache: &CompletionCache,
1514 type_id: &str,
1515 node_id: Option<NodeId>,
1516) -> Vec<CompletionItem> {
1517 if let Some(items) = cache.using_for.get(type_id) {
1519 return items.clone();
1520 }
1521
1522 if let Some(nid) = node_id
1526 && type_id.starts_with("__node_id_")
1527 && let Some((_, items)) = cache
1528 .using_for
1529 .iter()
1530 .find(|(tid, _)| extract_node_id_from_type(tid.as_str()) == Some(nid))
1531 {
1532 return items.clone();
1533 }
1534
1535 let base = strip_type_suffix(type_id);
1537 let variants = [
1538 base.to_string(),
1539 format!("{}_storage", base),
1540 format!("{}_storage_ptr", base),
1541 format!("{}_memory", base),
1542 format!("{}_memory_ptr", base),
1543 format!("{}_calldata", base),
1544 ];
1545 for variant in &variants {
1546 if variant.as_str() != type_id
1547 && let Some(items) = cache.using_for.get(variant.as_str())
1548 {
1549 return items.clone();
1550 }
1551 }
1552
1553 vec![]
1554}
1555
1556fn split_array_type(type_id: &str) -> Option<(&str, &str)> {
1562 let inner = strip_type_suffix(type_id).strip_prefix("t_array$_")?;
1563 inner.rsplit_once("_$")
1564}
1565
1566fn index_result_type(type_id: &str) -> Option<String> {
1569 if type_id.starts_with("t_mapping") {
1570 return extract_mapping_value_type(type_id);
1571 }
1572 if let Some((element, _)) = split_array_type(type_id) {
1573 return Some(element.to_string());
1574 }
1575 if strip_type_suffix(type_id) == "t_bytes" {
1576 return Some("t_bytes1".to_string());
1577 }
1578 None
1579}
1580
1581fn builtin_type_members(type_id: &str) -> Vec<CompletionItem> {
1584 let is_dynamic_bytes = strip_type_suffix(type_id) == "t_bytes";
1585 let is_dynamic_array = split_array_type(type_id).is_some_and(|(_, len)| len == "dyn");
1586 let is_fixed = split_array_type(type_id).is_some_and(|(_, len)| len != "dyn")
1587 || type_id
1588 .strip_prefix("t_bytes")
1589 .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
1590
1591 let mut items = Vec::new();
1592 if is_dynamic_bytes || is_dynamic_array || is_fixed {
1593 items.push(builtin_member(
1594 "length",
1595 CompletionItemKind::PROPERTY,
1596 Some("uint256"),
1597 ));
1598 }
1599 if (is_dynamic_bytes || is_dynamic_array) && type_id.contains("_storage") {
1600 items.push(builtin_member(
1601 "push(...)",
1602 CompletionItemKind::METHOD,
1603 None,
1604 ));
1605 items.push(builtin_member("pop()", CompletionItemKind::METHOD, None));
1606 }
1607 items
1608}
1609
1610const SELECTOR_MEMBER_TYPE: &str = "__selector_member";
1613
1614fn completions_for_type(
1617 cache: &CompletionCache,
1618 type_id: &str,
1619 receiver: ReceiverKind,
1620) -> Vec<CompletionItem> {
1621 if type_id == SELECTOR_MEMBER_TYPE {
1622 return vec![builtin_member(
1623 "selector",
1624 CompletionItemKind::PROPERTY,
1625 Some("bytes4"),
1626 )];
1627 }
1628
1629 if type_id == "t_address" || type_id == "t_address_payable" {
1631 let mut items = address_members();
1632 if let Some(uf) = cache.using_for.get(type_id) {
1634 items.extend(uf.iter().cloned());
1635 }
1636 items.extend(cache.using_for_wildcard.iter().cloned());
1637 return items;
1638 }
1639
1640 let resolved_node_id = extract_node_id_from_type(type_id)
1641 .or_else(|| cache.type_to_node.get(type_id).copied())
1642 .or_else(|| {
1643 type_id
1645 .strip_prefix("__node_id_")
1646 .and_then(|s| s.parse::<i64>().ok())
1647 .map(NodeId)
1648 });
1649
1650 let mut items = builtin_type_members(type_id);
1651 let mut seen_labels: std::collections::HashSet<String> =
1652 items.iter().map(|item| item.label.clone()).collect();
1653
1654 if let Some(node_id) = resolved_node_id {
1655 if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1657 for item in method_items {
1658 seen_labels.insert(item.label.clone());
1659 items.push(item.clone());
1660 }
1661 }
1662
1663 if let Some(members) = cache.node_members.get(&node_id) {
1665 for item in members {
1666 if !seen_labels.contains(&item.label) {
1667 seen_labels.insert(item.label.clone());
1668 items.push(item.clone());
1669 }
1670 }
1671 }
1672 }
1673
1674 if receiver == ReceiverKind::Value {
1683 let uf_items = lookup_using_for(cache, type_id, resolved_node_id);
1685 for item in &uf_items {
1686 if !seen_labels.contains(&item.label) {
1687 seen_labels.insert(item.label.clone());
1688 items.push(item.clone());
1689 }
1690 }
1691
1692 for item in &cache.using_for_wildcard {
1694 if !seen_labels.contains(&item.label) {
1695 seen_labels.insert(item.label.clone());
1696 items.push(item.clone());
1697 }
1698 }
1699 }
1700
1701 items
1702}
1703
1704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1711enum ReceiverKind {
1712 TypeName,
1714 Value,
1716}
1717
1718fn resolve_name_to_type_id_kind(
1722 cache: &CompletionCache,
1723 name: &str,
1724) -> Option<(String, ReceiverKind)> {
1725 if let Some(tid) = cache.name_to_type.get(name) {
1727 return Some((tid.to_string(), ReceiverKind::Value));
1728 }
1729 if let Some(node_id) = cache.name_to_node_id.get(name) {
1731 for (tid, nid) in &cache.type_to_node {
1733 if nid == node_id {
1734 return Some((tid.to_string(), ReceiverKind::TypeName));
1735 }
1736 }
1737 return Some((format!("__node_id_{}", node_id), ReceiverKind::TypeName));
1739 }
1740 None
1741}
1742
1743pub fn map_live_offset_to_compiled(live: &str, compiled: &str, pos: usize) -> usize {
1754 let (l, c) = (live.as_bytes(), compiled.as_bytes());
1755 let prefix = l.iter().zip(c).take_while(|(a, b)| a == b).count();
1756 if pos <= prefix {
1757 return pos;
1758 }
1759 let max_suffix = l.len().min(c.len()) - prefix;
1760 let suffix = l
1761 .iter()
1762 .rev()
1763 .zip(c.iter().rev())
1764 .take(max_suffix)
1765 .take_while(|(a, b)| a == b)
1766 .count();
1767 let shifted = (pos + c.len()).saturating_sub(l.len());
1768 if pos >= l.len() - suffix {
1769 return shifted;
1770 }
1771 shifted.clamp(prefix, c.len() - suffix)
1772}
1773
1774fn append_scoped_variables(
1781 cache: &CompletionCache,
1782 items: &mut Vec<CompletionItem>,
1783 ctx: Option<&ScopeContext>,
1784) {
1785 let mut seen: HashSet<&str> = HashSet::new();
1786 let mut scoped = Vec::new();
1787 let in_scope = ctx.is_some_and(|ctx| {
1788 walk_visible_declarations(cache, ctx.byte_pos, ctx.file_id, |decl| {
1789 if !cache.general_labels.contains(&decl.name) && seen.insert(&decl.name) {
1790 scoped.push(CompletionItem {
1791 label: decl.name.clone(),
1792 kind: Some(CompletionItemKind::VARIABLE),
1793 detail: decl.type_string.clone(),
1794 ..Default::default()
1795 });
1796 }
1797 ControlFlow::Continue(())
1798 })
1799 });
1800 if in_scope {
1801 items.extend(scoped);
1802 } else {
1803 items.extend(cache.unscoped_variables.iter().cloned());
1804 }
1805}
1806
1807fn walk_visible_declarations<'a>(
1815 cache: &'a CompletionCache,
1816 byte_pos: usize,
1817 file_id: FileId,
1818 mut visit: impl FnMut(&'a ScopedDeclaration) -> ControlFlow<()>,
1819) -> bool {
1820 let Some(mut current) = find_innermost_scope(cache, byte_pos, file_id) else {
1821 return false;
1822 };
1823 let declared_before =
1824 |decl: &ScopedDeclaration| decl.declared_at.is_none_or(|at| at < byte_pos);
1825 loop {
1826 for decl in cache.scope_declarations.get(¤t).into_iter().flatten() {
1827 if declared_before(decl) && visit(decl).is_break() {
1828 return true;
1829 }
1830 }
1831 for &base in cache
1832 .linearized_base_contracts
1833 .get(¤t)
1834 .into_iter()
1835 .flatten()
1836 .skip(1)
1837 {
1838 for decl in cache.scope_declarations.get(&base).into_iter().flatten() {
1839 if !decl.is_private && visit(decl).is_break() {
1840 return true;
1841 }
1842 }
1843 }
1844 match cache.scope_parent.get(¤t) {
1845 Some(&parent) => current = parent,
1846 None => return true,
1847 }
1848 }
1849}
1850
1851pub fn find_innermost_scope(
1855 cache: &CompletionCache,
1856 byte_pos: usize,
1857 file_id: FileId,
1858) -> Option<NodeId> {
1859 cache
1861 .scope_ranges
1862 .iter()
1863 .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1864 .map(|r| r.node_id)
1865}
1866
1867pub fn resolve_name_in_scope(
1876 cache: &CompletionCache,
1877 name: &str,
1878 byte_pos: usize,
1879 file_id: FileId,
1880) -> Option<String> {
1881 resolve_name_in_scope_kind(cache, name, byte_pos, file_id).map(|(tid, _)| tid)
1882}
1883
1884fn resolve_name_in_scope_kind(
1887 cache: &CompletionCache,
1888 name: &str,
1889 byte_pos: usize,
1890 file_id: FileId,
1891) -> Option<(String, ReceiverKind)> {
1892 let mut found = None;
1893 walk_visible_declarations(cache, byte_pos, file_id, |decl| {
1894 if decl.name == name {
1895 found = Some((decl.type_id.clone(), ReceiverKind::Value));
1896 return ControlFlow::Break(());
1897 }
1898 ControlFlow::Continue(())
1899 });
1900
1901 found.or_else(|| resolve_name_to_type_id_kind(cache, name))
1904}
1905
1906fn resolve_member_type(
1911 cache: &CompletionCache,
1912 context_type_id: &str,
1913 member_name: &str,
1914 kind: &AccessKind,
1915) -> Option<String> {
1916 let resolved_node_id = extract_node_id_from_type(context_type_id)
1917 .or_else(|| cache.type_to_node.get(context_type_id).copied())
1918 .or_else(|| {
1919 context_type_id
1921 .strip_prefix("__node_id_")
1922 .and_then(|s| s.parse::<i64>().ok())
1923 .map(NodeId)
1924 });
1925
1926 let node_id = resolved_node_id?;
1927
1928 match kind {
1929 AccessKind::Call => {
1930 cache
1932 .function_return_types
1933 .get(&(node_id, member_name.to_string()))
1934 .cloned()
1935 }
1936 AccessKind::Index => {
1937 if let Some(members) = cache.node_members.get(&node_id) {
1939 for member in members {
1940 if member.label == member_name {
1941 if let Some(tid) = cache.name_to_type.get(member_name) {
1943 return index_result_type(tid.as_str()).or(Some(tid.to_string()));
1944 }
1945 }
1946 }
1947 }
1948 cache
1950 .name_to_type
1951 .get(member_name)
1952 .and_then(|tid| index_result_type(tid.as_str()))
1953 }
1954 AccessKind::Plain => {
1955 let is_callable_member = cache.node_members.get(&node_id).is_some_and(|members| {
1957 members.iter().any(|m| {
1958 m.label == member_name
1959 && matches!(
1960 m.kind,
1961 Some(CompletionItemKind::FUNCTION | CompletionItemKind::EVENT)
1962 )
1963 })
1964 }) || cache
1965 .method_identifiers
1966 .get(&node_id)
1967 .is_some_and(|methods| methods.iter().any(|m| m.label == member_name));
1968 if is_callable_member {
1969 return Some(SELECTOR_MEMBER_TYPE.to_string());
1970 }
1971 if let Some(enum_id) = cache
1973 .nested_types
1974 .get(&(node_id, SymbolName::new(member_name)))
1975 {
1976 return Some(format!("__node_id_{}", enum_id));
1977 }
1978 cache
1980 .name_to_type
1981 .get(member_name)
1982 .map(|tid| tid.to_string())
1983 }
1984 }
1985}
1986
1987pub struct ScopeContext {
1991 pub byte_pos: usize,
1993 pub file_id: FileId,
1995}
1996
1997fn resolve_name(
2001 cache: &CompletionCache,
2002 name: &str,
2003 scope_ctx: Option<&ScopeContext>,
2004) -> Option<String> {
2005 resolve_name_kind(cache, name, scope_ctx).map(|(tid, _)| tid)
2006}
2007
2008fn resolve_name_kind(
2010 cache: &CompletionCache,
2011 name: &str,
2012 scope_ctx: Option<&ScopeContext>,
2013) -> Option<(String, ReceiverKind)> {
2014 if let Some(ctx) = scope_ctx {
2015 resolve_name_in_scope_kind(cache, name, ctx.byte_pos, ctx.file_id)
2016 } else {
2017 resolve_name_to_type_id_kind(cache, name)
2018 }
2019}
2020
2021pub fn get_dot_completions(
2023 cache: &CompletionCache,
2024 identifier: &str,
2025 scope_ctx: Option<&ScopeContext>,
2026) -> Vec<CompletionItem> {
2027 if let Some(items) = magic_members(identifier) {
2029 return items;
2030 }
2031 if let Some(items) = cache.type_name_members.get(identifier) {
2032 return items.clone();
2033 }
2034
2035 if let Some((tid, receiver)) = resolve_name_kind(cache, identifier, scope_ctx) {
2038 return completions_for_type(cache, &tid, receiver);
2039 }
2040
2041 vec![]
2042}
2043
2044pub fn get_chain_completions(
2047 cache: &CompletionCache,
2048 chain: &[DotSegment],
2049 scope_ctx: Option<&ScopeContext>,
2050) -> Vec<CompletionItem> {
2051 if chain.is_empty() {
2052 return vec![];
2053 }
2054
2055 if chain.len() == 1 {
2057 let seg = &chain[0];
2058
2059 match seg.kind {
2061 AccessKind::Plain => {
2062 return get_dot_completions(cache, &seg.name, scope_ctx);
2063 }
2064 AccessKind::Call => {
2065 if seg.name == "type" {
2067 return type_meta_members(seg.call_args.as_deref(), Some(cache));
2068 }
2069 if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
2074 return completions_for_type(cache, &type_id, ReceiverKind::Value);
2075 }
2076 for ((_, fn_name), ret_type) in &cache.function_return_types {
2078 if fn_name == &seg.name {
2079 return completions_for_type(cache, ret_type, ReceiverKind::Value);
2080 }
2081 }
2082 return vec![];
2083 }
2084 AccessKind::Index => {
2085 if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
2087 && let Some(val_type) = index_result_type(&tid)
2088 {
2089 return completions_for_type(cache, &val_type, ReceiverKind::Value);
2090 }
2091 return vec![];
2092 }
2093 }
2094 }
2095
2096 let first = &chain[0];
2099 let mut current_type = match first.kind {
2100 AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
2101 AccessKind::Call => {
2102 resolve_name(cache, &first.name, scope_ctx).or_else(|| {
2104 cache
2105 .function_return_types
2106 .iter()
2107 .find(|((_, fn_name), _)| fn_name == &first.name)
2108 .map(|(_, ret_type)| ret_type.clone())
2109 })
2110 }
2111 AccessKind::Index => {
2112 resolve_name(cache, &first.name, scope_ctx)
2114 .and_then(|tid| index_result_type(&tid).or(Some(tid)))
2115 }
2116 };
2117
2118 for seg in &chain[1..] {
2120 let ctx_type = match ¤t_type {
2121 Some(t) => t.clone(),
2122 None => return vec![],
2123 };
2124
2125 current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
2126 }
2127
2128 match current_type {
2132 Some(tid) => {
2133 let receiver = if tid.starts_with("__node_id_") {
2134 ReceiverKind::TypeName
2135 } else {
2136 ReceiverKind::Value
2137 };
2138 completions_for_type(cache, &tid, receiver)
2139 }
2140 None => vec![],
2141 }
2142}
2143
2144pub fn get_static_completions() -> Vec<CompletionItem> {
2147 let mut items = Vec::new();
2148
2149 for kw in SOLIDITY_KEYWORDS {
2151 items.push(CompletionItem {
2152 label: kw.to_string(),
2153 kind: Some(CompletionItemKind::KEYWORD),
2154 ..Default::default()
2155 });
2156 }
2157
2158 for (name, detail) in MAGIC_GLOBALS {
2160 items.push(CompletionItem {
2161 label: name.to_string(),
2162 kind: Some(CompletionItemKind::VARIABLE),
2163 detail: Some(detail.to_string()),
2164 ..Default::default()
2165 });
2166 }
2167
2168 for (name, detail) in GLOBAL_FUNCTIONS {
2170 items.push(CompletionItem {
2171 label: name.to_string(),
2172 kind: Some(CompletionItemKind::FUNCTION),
2173 detail: Some(detail.to_string()),
2174 ..Default::default()
2175 });
2176 }
2177
2178 for (name, detail) in ETHER_UNITS {
2180 items.push(CompletionItem {
2181 label: name.to_string(),
2182 kind: Some(CompletionItemKind::UNIT),
2183 detail: Some(detail.to_string()),
2184 ..Default::default()
2185 });
2186 }
2187
2188 for (name, detail) in TIME_UNITS {
2190 items.push(CompletionItem {
2191 label: name.to_string(),
2192 kind: Some(CompletionItemKind::UNIT),
2193 detail: Some(detail.to_string()),
2194 ..Default::default()
2195 });
2196 }
2197
2198 items
2199}
2200
2201pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
2203 let mut items = cache.names.clone();
2204 items.extend(get_static_completions());
2205 items
2206}
2207
2208fn completion_item(
2209 label: impl Into<String>,
2210 kind: CompletionItemKind,
2211 detail: impl Into<String>,
2212) -> CompletionItem {
2213 CompletionItem {
2214 label: label.into(),
2215 kind: Some(kind),
2216 detail: Some(detail.into()),
2217 ..Default::default()
2218 }
2219}
2220
2221fn line_replacement_item(
2222 label: impl Into<String>,
2223 detail: impl Into<String>,
2224 new_text: impl Into<String>,
2225 position: Position,
2226) -> CompletionItem {
2227 let label = label.into();
2228 let new_text = new_text.into();
2229 CompletionItem {
2230 filter_text: Some(format!("spdx {label} {new_text}")),
2231 label,
2232 kind: Some(CompletionItemKind::CONSTANT),
2233 detail: Some(detail.into()),
2234 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
2235 range: Range {
2236 start: Position {
2237 line: position.line,
2238 character: 0,
2239 },
2240 end: position,
2241 },
2242 new_text,
2243 })),
2244 ..Default::default()
2245 }
2246}
2247
2248fn prefix_at_byte(line: &str, col_byte: u32) -> &str {
2249 let mut end = (col_byte as usize).min(line.len());
2250 while !line.is_char_boundary(end) {
2251 end -= 1;
2252 }
2253 &line[..end]
2254}
2255
2256fn source_unit_directive_completions(
2257 line: &str,
2258 line_prefix: &str,
2259 position: Position,
2260) -> Option<Vec<CompletionItem>> {
2261 let trimmed = line_prefix.trim_start();
2262
2263 if let Some(after_slashes) = trimmed.strip_prefix("//") {
2264 let body = after_slashes.trim_start();
2265 let upper = body.to_ascii_uppercase();
2266
2267 if upper.starts_with("SPDX-LICENSE-IDENTIFIER:") {
2268 return Some(spdx_license_identifier_completions());
2269 }
2270
2271 if is_spdx_directive_prefix(&upper) {
2272 return Some(spdx_directive_completions(position));
2273 }
2274 }
2275
2276 if let Some(after_solidity) = strip_directive_value(trimmed, "pragma solidity") {
2277 if after_solidity.contains(';') {
2278 return Some(vec![]);
2279 }
2280
2281 return Some(solidity_version_completions(
2282 position,
2283 directive_value_range(line, "pragma solidity"),
2284 ));
2285 }
2286
2287 if let Some(after_abicoder) = strip_directive_value(trimmed, "pragma abicoder") {
2288 if after_abicoder.contains(';') {
2289 return Some(vec![]);
2290 }
2291
2292 return Some(vec![
2293 completion_item("v2", CompletionItemKind::CONSTANT, "ABI coder v2"),
2294 completion_item("v1", CompletionItemKind::CONSTANT, "ABI coder v1"),
2295 ]);
2296 }
2297
2298 if let Some(after_pragma) = trimmed.strip_prefix("pragma")
2299 && !after_pragma.is_empty()
2300 && after_pragma.trim().is_empty()
2301 {
2302 return Some(vec![
2303 completion_item(
2304 "solidity",
2305 CompletionItemKind::KEYWORD,
2306 "Compiler version pragma",
2307 ),
2308 completion_item("abicoder", CompletionItemKind::KEYWORD, "ABI coder pragma"),
2309 ]);
2310 }
2311
2312 None
2313}
2314
2315fn strip_directive_value<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
2316 let rest = line.strip_prefix(directive)?;
2317 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
2318 Some(rest)
2319 } else {
2320 None
2321 }
2322}
2323
2324fn is_spdx_directive_prefix(upper_comment_body: &str) -> bool {
2325 let body = upper_comment_body.trim_start();
2326 !body.is_empty() && ("SPDX".starts_with(body) || body.starts_with("SPDX"))
2327}
2328
2329fn spdx_directive_completions(position: Position) -> Vec<CompletionItem> {
2330 vec![
2331 line_replacement_item(
2332 "// SPDX-License-Identifier: MIT",
2333 "SPDX license identifier",
2334 "// SPDX-License-Identifier: MIT",
2335 position,
2336 ),
2337 line_replacement_item(
2338 "// SPDX-License-Identifier: UNLICENSED",
2339 "SPDX license identifier",
2340 "// SPDX-License-Identifier: UNLICENSED",
2341 position,
2342 ),
2343 line_replacement_item(
2344 "// SPDX-License-Identifier: Apache-2.0",
2345 "SPDX license identifier",
2346 "// SPDX-License-Identifier: Apache-2.0",
2347 position,
2348 ),
2349 ]
2350}
2351
2352fn spdx_license_identifier_completions() -> Vec<CompletionItem> {
2353 [
2354 "MIT",
2355 "UNLICENSED",
2356 "Apache-2.0",
2357 "GPL-3.0-only",
2358 "GPL-3.0-or-later",
2359 "BSD-3-Clause",
2360 ]
2361 .into_iter()
2362 .map(|license| completion_item(license, CompletionItemKind::CONSTANT, "SPDX license"))
2363 .collect()
2364}
2365
2366fn directive_value_range(line: &str, directive: &str) -> Range {
2367 let mut byte = line
2368 .find(directive)
2369 .map(|idx| idx + directive.len())
2370 .unwrap_or(line.len());
2371
2372 while byte < line.len() {
2373 let ch = line[byte..]
2374 .chars()
2375 .next()
2376 .expect("byte is within string bounds");
2377 if !ch.is_whitespace() {
2378 break;
2379 }
2380 byte += ch.len_utf8();
2381 }
2382
2383 let end_byte = line[byte..]
2384 .find(';')
2385 .map(|idx| byte + idx)
2386 .unwrap_or(line.len());
2387
2388 Range {
2389 start: Position {
2390 line: 0,
2391 character: line[..byte].encode_utf16().count() as u32,
2392 },
2393 end: Position {
2394 line: 0,
2395 character: line[..end_byte].encode_utf16().count() as u32,
2396 },
2397 }
2398}
2399
2400fn version_completion_item(
2401 label: impl Into<String>,
2402 detail: impl Into<String>,
2403 position: Position,
2404 range: Range,
2405) -> CompletionItem {
2406 let label = label.into();
2407 CompletionItem {
2408 label: label.clone(),
2409 kind: Some(CompletionItemKind::CONSTANT),
2410 detail: Some(detail.into()),
2411 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
2412 range: Range {
2413 start: Position {
2414 line: position.line,
2415 character: range.start.character,
2416 },
2417 end: Position {
2418 line: position.line,
2419 character: range.end.character,
2420 },
2421 },
2422 new_text: label,
2423 })),
2424 ..Default::default()
2425 }
2426}
2427
2428fn solidity_version_completions(position: Position, range: Range) -> Vec<CompletionItem> {
2429 [
2430 ("^0.8.35", "Latest Solidity 0.8.x compatible range"),
2431 ("0.8.35", "Latest Solidity exact version"),
2432 ("^0.8.0", "Solidity 0.8.x compatible range"),
2433 (">=0.8.0 <0.9.0", "Solidity 0.8.x range"),
2434 ]
2435 .into_iter()
2436 .map(|(version, detail)| version_completion_item(version, detail, position, range))
2437 .collect()
2438}
2439
2440pub fn append_auto_import_candidates_last(
2446 mut base: Vec<CompletionItem>,
2447 mut auto_import_candidates: Vec<CompletionItem>,
2448) -> Vec<CompletionItem> {
2449 let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
2450 for item in &auto_import_candidates {
2451 let entry = unique_label_edits
2452 .entry(item.label.clone())
2453 .or_insert_with(|| item.additional_text_edits.clone());
2454 if *entry != item.additional_text_edits {
2455 *entry = None;
2456 }
2457 }
2458
2459 for item in &mut base {
2463 if item.additional_text_edits.is_none()
2464 && let Some(Some(edits)) = unique_label_edits.get(&item.label)
2465 {
2466 item.additional_text_edits = Some(edits.clone());
2467 }
2468 }
2469
2470 for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
2471 if item.sort_text.is_none() {
2472 item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
2473 }
2474 }
2475
2476 base.extend(auto_import_candidates);
2477 base
2478}
2479
2480pub fn top_level_importable_completion_candidates(
2485 cache: &CompletionCache,
2486 current_file_path: Option<&str>,
2487 source_text: &str,
2488) -> Vec<CompletionItem> {
2489 let mut out = Vec::new();
2490 for symbols in cache.top_level_importables_by_name.values() {
2491 for symbol in symbols {
2492 if let Some(cur) = current_file_path
2493 && cur == symbol.declaring_path
2494 {
2495 continue;
2496 }
2497
2498 let import_path = match current_file_path.and_then(|cur| {
2499 to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
2500 }) {
2501 Some(p) => p,
2502 None => continue,
2503 };
2504
2505 if import_statement_already_present(source_text, &symbol.name, &import_path) {
2506 continue;
2507 }
2508
2509 let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
2510 out.push(CompletionItem {
2511 label: symbol.name.clone(),
2512 kind: Some(symbol.kind),
2513 detail: Some(format!("{} ({import_path})", symbol.node_type)),
2514 additional_text_edits: import_edit.map(|e| vec![e]),
2515 ..Default::default()
2516 });
2517 }
2518 }
2519 out
2520}
2521
2522fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
2523 let from_dir = current_file.parent()?;
2524 let rel = pathdiff::diff_paths(target_file, from_dir)?;
2525 let mut s = rel.to_string_lossy().replace('\\', "/");
2526 if !s.starts_with("./") && !s.starts_with("../") {
2527 s = format!("./{s}");
2528 }
2529 Some(s)
2530}
2531
2532fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
2533 let named = format!("import {{{symbol}}} from \"{import_path}\";");
2534 let full = format!("import \"{import_path}\";");
2535 source_text.contains(&named) || source_text.contains(&full)
2536}
2537
2538fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
2539 let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
2540 let lines: Vec<&str> = source_text.lines().collect();
2541
2542 let last_import_line = lines
2543 .iter()
2544 .enumerate()
2545 .filter(|(_, line)| line.trim_start().starts_with("import "))
2546 .map(|(idx, _)| idx)
2547 .next_back();
2548
2549 let insert_line = if let Some(idx) = last_import_line {
2550 idx + 1
2551 } else if let Some(idx) = lines
2552 .iter()
2553 .enumerate()
2554 .filter(|(_, line)| line.trim_start().starts_with("pragma "))
2555 .map(|(idx, _)| idx)
2556 .next_back()
2557 {
2558 idx + 1
2559 } else {
2560 0
2561 };
2562
2563 Some(TextEdit {
2564 range: Range {
2565 start: Position {
2566 line: insert_line as u32,
2567 character: 0,
2568 },
2569 end: Position {
2570 line: insert_line as u32,
2571 character: 0,
2572 },
2573 },
2574 new_text: import_stmt,
2575 })
2576}
2577
2578pub fn handle_completion_with_tail_candidates(
2583 cache: Option<&CompletionCache>,
2584 source_text: &str,
2585 position: Position,
2586 trigger_char: Option<&str>,
2587 file_id: Option<FileId>,
2588 compiled_text: Option<&str>,
2589 tail_candidates: Vec<CompletionItem>,
2590) -> Option<CompletionResponse> {
2591 let lines: Vec<&str> = source_text.lines().collect();
2592 let line = lines.get(position.line as usize)?;
2593
2594 let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
2596 let line_start_byte: usize = source_text[..abs_byte]
2597 .rfind('\n')
2598 .map(|i| i + 1)
2599 .unwrap_or(0);
2600 let col_byte = (abs_byte - line_start_byte) as u32;
2601
2602 if let Some(items) =
2603 source_unit_directive_completions(line, prefix_at_byte(line, col_byte), position)
2604 {
2605 return Some(CompletionResponse::List(CompletionList {
2606 is_incomplete: false,
2607 items,
2608 }));
2609 }
2610
2611 let scope_ctx = file_id.map(|fid| ScopeContext {
2614 byte_pos: compiled_text.map_or(abs_byte, |ct| {
2615 map_live_offset_to_compiled(source_text, ct, abs_byte)
2616 }),
2617 file_id: fid,
2618 });
2619
2620 let triggered_by_dot = trigger_char == Some(".");
2621 let member_items = if triggered_by_dot || is_member_completion(line, col_byte) {
2622 let chain = crate::completion::parse_dot_chain(line, col_byte);
2623 if chain.is_empty() && triggered_by_dot {
2624 return None;
2625 }
2626 Some(match cache {
2627 Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
2628 None => {
2629 if chain.len() == 1 {
2631 let seg = &chain[0];
2632 if seg.name == "type" && seg.kind == AccessKind::Call {
2633 type_meta_members(seg.call_args.as_deref(), None)
2635 } else if seg.kind == AccessKind::Plain {
2636 magic_members(&seg.name).unwrap_or_default()
2637 } else {
2638 vec![]
2639 }
2640 } else {
2641 vec![]
2642 }
2643 }
2644 })
2645 } else {
2646 None
2647 };
2648
2649 let items = match member_items {
2650 Some(items) if triggered_by_dot || !items.is_empty() => items,
2654 _ => match cache {
2655 Some(c) => {
2656 let mut items = c.general_completions.clone();
2657 append_scoped_variables(c, &mut items, scope_ctx.as_ref());
2658 append_auto_import_candidates_last(items, tail_candidates)
2659 }
2660 None => get_static_completions(),
2661 },
2662 };
2663
2664 Some(CompletionResponse::List(CompletionList {
2665 is_incomplete: cache.is_none(),
2666 items,
2667 }))
2668}
2669
2670pub fn handle_completion(
2680 cache: Option<&CompletionCache>,
2681 source_text: &str,
2682 position: Position,
2683 trigger_char: Option<&str>,
2684 file_id: Option<FileId>,
2685) -> Option<CompletionResponse> {
2686 handle_completion_with_tail_candidates(
2687 cache,
2688 source_text,
2689 position,
2690 trigger_char,
2691 file_id,
2692 None,
2693 vec![],
2694 )
2695}
2696
2697const SOLIDITY_KEYWORDS: &[&str] = &[
2698 "abstract",
2699 "address",
2700 "assembly",
2701 "bool",
2702 "break",
2703 "bytes",
2704 "bytes1",
2705 "bytes4",
2706 "bytes32",
2707 "calldata",
2708 "constant",
2709 "constructor",
2710 "continue",
2711 "contract",
2712 "delete",
2713 "do",
2714 "else",
2715 "emit",
2716 "enum",
2717 "error",
2718 "event",
2719 "external",
2720 "fallback",
2721 "false",
2722 "for",
2723 "function",
2724 "if",
2725 "immutable",
2726 "import",
2727 "indexed",
2728 "int8",
2729 "int24",
2730 "int128",
2731 "int256",
2732 "interface",
2733 "internal",
2734 "library",
2735 "mapping",
2736 "memory",
2737 "modifier",
2738 "new",
2739 "override",
2740 "payable",
2741 "pragma",
2742 "private",
2743 "public",
2744 "pure",
2745 "receive",
2746 "return",
2747 "returns",
2748 "revert",
2749 "storage",
2750 "string",
2751 "struct",
2752 "true",
2753 "type",
2754 "uint8",
2755 "uint24",
2756 "uint128",
2757 "uint160",
2758 "uint256",
2759 "unchecked",
2760 "using",
2761 "view",
2762 "virtual",
2763 "while",
2764];
2765
2766const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2768
2769const TIME_UNITS: &[(&str, &str)] = &[
2771 ("seconds", "1"),
2772 ("minutes", "60 seconds"),
2773 ("hours", "3600 seconds"),
2774 ("days", "86400 seconds"),
2775 ("weeks", "604800 seconds"),
2776];
2777
2778const MAGIC_GLOBALS: &[(&str, &str)] = &[
2779 ("msg", "msg"),
2780 ("block", "block"),
2781 ("tx", "tx"),
2782 ("abi", "abi"),
2783 ("this", "address"),
2784 ("super", "contract"),
2785 ("type", "type information"),
2786];
2787
2788const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2789 ("addmod(uint256, uint256, uint256)", "uint256"),
2791 ("mulmod(uint256, uint256, uint256)", "uint256"),
2792 ("keccak256(bytes memory)", "bytes32"),
2793 ("sha256(bytes memory)", "bytes32"),
2794 ("ripemd160(bytes memory)", "bytes20"),
2795 (
2796 "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2797 "address",
2798 ),
2799 ("blockhash(uint256 blockNumber)", "bytes32"),
2801 ("blobhash(uint256 index)", "bytes32"),
2802 ("gasleft()", "uint256"),
2803 ("assert(bool condition)", ""),
2805 ("require(bool condition)", ""),
2806 ("require(bool condition, string memory message)", ""),
2807 ("revert()", ""),
2808 ("revert(string memory reason)", ""),
2809 ("selfdestruct(address payable recipient)", ""),
2811];
2812
2813pub fn all_sol_import_paths(
2829 current_file: &Path,
2830 project_root: &Path,
2831 remappings: &[String],
2832 typed_range: Option<(u32, u32, u32)>,
2833) -> Vec<CompletionItem> {
2834 let current_dir = match current_file.parent() {
2835 Some(d) => d,
2836 None => return vec![],
2837 };
2838
2839 let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2844 .iter()
2845 .filter_map(|r| {
2846 let mut it = r.splitn(2, '=');
2847 let prefix = it.next()?.to_string();
2848 let target = it.next()?;
2849 if prefix.is_empty() || target.is_empty() {
2850 return None;
2851 }
2852 let raw = project_root.join(target);
2853 let canonical = raw.canonicalize().unwrap_or(raw);
2854 Some((prefix, canonical))
2855 })
2856 .collect();
2857
2858 let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2859 let mut items = Vec::new();
2860
2861 collect_sol_files(
2862 project_root,
2863 current_dir,
2864 &parsed_remappings,
2865 skip_dirs,
2866 typed_range,
2867 &mut items,
2868 );
2869 items.sort_by(|a, b| a.label.cmp(&b.label));
2870 items
2871}
2872
2873fn collect_sol_files(
2874 dir: &Path,
2875 current_dir: &Path,
2876 remappings: &[(String, std::path::PathBuf)],
2877 skip_dirs: &[&str],
2878 typed_range: Option<(u32, u32, u32)>,
2879 out: &mut Vec<CompletionItem>,
2880) {
2881 let entries = match std::fs::read_dir(dir) {
2882 Ok(e) => e,
2883 Err(_) => return,
2884 };
2885 for entry in entries.flatten() {
2886 let path = entry.path();
2887 let name = entry.file_name();
2888 let name_str = name.to_string_lossy();
2889
2890 if name_str.starts_with('.') {
2891 continue;
2892 }
2893
2894 if path.is_dir() {
2895 if skip_dirs.contains(&name_str.as_ref()) {
2896 continue;
2897 }
2898 collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2899 continue;
2900 }
2901
2902 if !path.is_file() || !name_str.ends_with(".sol") {
2903 continue;
2904 }
2905
2906 if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2908 let s = rel.to_string_lossy().to_string();
2909 let label = if s.starts_with("../") || s.starts_with("./") {
2910 s
2911 } else {
2912 format!("./{s}")
2913 };
2914 out.push(make_import_item(label, typed_range));
2915 }
2916
2917 for (prefix, target_abs) in remappings {
2923 if let Ok(suffix) = path.strip_prefix(target_abs) {
2924 let suffix_str = suffix
2925 .to_string_lossy()
2926 .trim_start_matches(['/', '\\'])
2927 .to_string();
2928 let label = format!("{}{}", prefix, suffix_str);
2929 out.push(make_import_item(label, typed_range));
2930 }
2931 }
2932 }
2933}
2934
2935fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2936 let text_edit = typed_range.map(|(line, start_col, end_col)| {
2943 tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2944 range: Range {
2945 start: Position {
2946 line,
2947 character: start_col,
2948 },
2949 end: Position {
2950 line,
2951 character: end_col,
2952 },
2953 },
2954 new_text: label.clone(),
2955 })
2956 });
2957 CompletionItem {
2958 label: label.clone(),
2959 filter_text: Some(label.clone()),
2960 text_edit,
2961 kind: Some(CompletionItemKind::FILE),
2962 ..Default::default()
2963 }
2964}
2965
2966#[cfg(test)]
2967mod tests {
2968 use super::{
2969 CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2970 build_completion_cache, extract_top_level_importables_for_file, handle_completion,
2971 };
2972 use crate::types::{FileId, SymbolName};
2973 use serde_json::json;
2974 use std::collections::{HashMap, HashSet};
2975 use tower_lsp::lsp_types::CompletionItemKind;
2976 use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2977
2978 fn empty_cache() -> CompletionCache {
2979 CompletionCache {
2980 names: vec![],
2981 name_to_type: HashMap::new(),
2982 node_members: HashMap::new(),
2983 type_to_node: HashMap::new(),
2984 name_to_node_id: HashMap::new(),
2985 nested_types: HashMap::new(),
2986 method_identifiers: HashMap::new(),
2987 function_return_types: HashMap::new(),
2988 using_for: HashMap::new(),
2989 using_for_wildcard: vec![],
2990 general_completions: vec![],
2991 general_labels: HashSet::new(),
2992 unscoped_variables: vec![],
2993 scope_declarations: HashMap::new(),
2994 scope_parent: HashMap::new(),
2995 scope_ranges: vec![],
2996 path_to_file_id: HashMap::new(),
2997 linearized_base_contracts: HashMap::new(),
2998 contract_kinds: HashMap::new(),
2999 type_name_members: HashMap::new(),
3000 top_level_importables_by_name: HashMap::new(),
3001 top_level_importables_by_file: HashMap::new(),
3002 }
3003 }
3004
3005 #[test]
3006 fn top_level_importables_include_only_direct_declared_symbols() {
3007 let sources = json!({
3008 "/tmp/A.sol": {
3009 "id": 0,
3010 "ast": {
3011 "id": 1,
3012 "nodeType": "SourceUnit",
3013 "src": "0:100:0",
3014 "nodes": [
3015 { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
3016 { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
3017 { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
3018 ] },
3019 { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
3020 { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
3021 { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
3022 { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
3023 { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
3024 { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
3025 ]
3026 }
3027 }
3028 });
3029
3030 let cache = build_completion_cache(&sources, None, None);
3031 let map = &cache.top_level_importables_by_name;
3032 let by_file = &cache.top_level_importables_by_file;
3033
3034 assert!(map.contains_key("C"));
3035 assert!(map.contains_key("S"));
3036 assert!(map.contains_key("E"));
3037 assert!(map.contains_key("Wad"));
3038 assert!(map.contains_key("freeFn"));
3039 assert!(map.contains_key("TOP_CONST"));
3040
3041 assert!(!map.contains_key("Alias"));
3042 assert!(!map.contains_key("inside"));
3043 assert!(!map.contains_key("TOP_VAR"));
3044
3045 let file_symbols = by_file.get("/tmp/A.sol").unwrap();
3046 let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
3047 assert!(file_names.contains(&"C"));
3048 assert!(file_names.contains(&"TOP_CONST"));
3049 assert!(!file_names.contains(&"Alias"));
3050 }
3051
3052 fn start_at_sources() -> serde_json::Value {
3053 json!({
3054 "/tmp/A.sol": {
3055 "id": 0,
3056 "ast": {
3057 "id": 1,
3058 "nodeType": "SourceUnit",
3059 "src": "0:200:0",
3060 "nodes": [
3061 {
3062 "id": 10,
3063 "nodeType": "StructDefinition",
3064 "name": "Contract",
3065 "scope": 1,
3066 "src": "0:30:0",
3067 "members": [
3068 {
3069 "id": 11,
3070 "nodeType": "VariableDeclaration",
3071 "name": "startAt",
3072 "scope": 10,
3073 "src": "10:7:0",
3074 "typeDescriptions": {
3075 "typeString": "uint32",
3076 "typeIdentifier": "t_uint32"
3077 }
3078 }
3079 ]
3080 },
3081 {
3082 "id": 30,
3083 "nodeType": "EventDefinition",
3084 "name": "Started",
3085 "scope": 1,
3086 "src": "31:20:0",
3087 "parameters": {
3088 "id": 31,
3089 "nodeType": "ParameterList",
3090 "src": "40:10:0",
3091 "parameters": [
3092 {
3093 "id": 32,
3094 "nodeType": "VariableDeclaration",
3095 "name": "startAt",
3096 "scope": 30,
3097 "src": "40:7:0",
3098 "typeDescriptions": {
3099 "typeString": "uint32",
3100 "typeIdentifier": "t_uint32"
3101 }
3102 }
3103 ]
3104 }
3105 },
3106 {
3107 "id": 40,
3108 "nodeType": "ContractDefinition",
3109 "name": "Pool",
3110 "scope": 1,
3111 "src": "60:140:0",
3112 "nodes": [
3113 {
3114 "id": 41,
3115 "nodeType": "VariableDeclaration",
3116 "name": "stateVar",
3117 "scope": 40,
3118 "src": "70:8:0",
3119 "typeDescriptions": {
3120 "typeString": "uint256",
3121 "typeIdentifier": "t_uint256"
3122 }
3123 },
3124 {
3125 "id": 50,
3126 "nodeType": "FunctionDefinition",
3127 "name": "onStart",
3128 "scope": 40,
3129 "src": "90:100:0",
3130 "parameters": {
3131 "id": 49,
3132 "nodeType": "ParameterList",
3133 "src": "95:12:0",
3134 "parameters": [
3135 {
3136 "id": 51,
3137 "nodeType": "VariableDeclaration",
3138 "name": "startAt",
3139 "scope": 50,
3140 "src": "100:7:0",
3141 "typeDescriptions": {
3142 "typeString": "uint32",
3143 "typeIdentifier": "t_uint32"
3144 }
3145 }
3146 ]
3147 },
3148 "body": {
3149 "id": 60,
3150 "nodeType": "Block",
3151 "scope": 50,
3152 "src": "110:70:0"
3153 }
3154 }
3155 ]
3156 }
3157 ]
3158 }
3159 }
3160 })
3161 }
3162
3163 fn labels_of(resp: Option<CompletionResponse>) -> Vec<String> {
3164 match resp {
3165 Some(CompletionResponse::List(list)) => {
3166 list.items.into_iter().map(|item| item.label).collect()
3167 }
3168 Some(CompletionResponse::Array(items)) => {
3169 items.into_iter().map(|item| item.label).collect()
3170 }
3171 None => Vec::new(),
3172 }
3173 }
3174
3175 #[test]
3176 fn general_completions_omit_struct_fields_and_params() {
3177 let cache = build_completion_cache(&start_at_sources(), None, None);
3178 let labels: Vec<&str> = cache
3179 .general_completions
3180 .iter()
3181 .map(|item| item.label.as_str())
3182 .collect();
3183
3184 assert!(labels.contains(&"Contract"));
3185 assert!(labels.contains(&"Pool"));
3186 assert!(labels.contains(&"onStart"));
3187 assert!(labels.contains(&"Started"));
3188 assert!(labels.contains(&"stateVar"));
3189 assert!(!labels.contains(&"startAt"));
3190 assert!(cache.name_to_type.contains_key("startAt"));
3193
3194 let members = cache.node_members.get(&crate::types::NodeId(10)).unwrap();
3195 assert!(members.iter().any(|item| item.label == "startAt"));
3196 }
3197
3198 #[test]
3199 fn param_completion_only_inside_its_function() {
3200 let cache = build_completion_cache(&start_at_sources(), None, None);
3201 let source = " ".repeat(200);
3202 let outside = labels_of(handle_completion(
3203 Some(&cache),
3204 &source,
3205 Position {
3206 line: 0,
3207 character: 10,
3208 },
3209 None,
3210 Some(FileId(0)),
3211 ));
3212 let inside = labels_of(handle_completion(
3213 Some(&cache),
3214 &source,
3215 Position {
3216 line: 0,
3217 character: 120,
3218 },
3219 None,
3220 Some(FileId(0)),
3221 ));
3222
3223 assert!(!outside.iter().any(|label| label == "startAt"));
3224 assert!(inside.iter().any(|label| label == "startAt"));
3225 }
3226
3227 #[test]
3228 fn member_completion_without_trigger_character() {
3229 let sources = json!({
3230 "/tmp/A.sol": {
3231 "id": 0,
3232 "ast": {
3233 "id": 1,
3234 "nodeType": "SourceUnit",
3235 "src": "0:80:0",
3236 "nodes": [
3237 {
3238 "id": 10,
3239 "nodeType": "StructDefinition",
3240 "name": "Info",
3241 "scope": 1,
3242 "src": "0:40:0",
3243 "members": [
3244 {
3245 "id": 11,
3246 "nodeType": "VariableDeclaration",
3247 "name": "startAt",
3248 "scope": 10,
3249 "src": "10:7:0",
3250 "typeDescriptions": {
3251 "typeString": "uint32",
3252 "typeIdentifier": "t_uint32"
3253 }
3254 }
3255 ],
3256 "typeDescriptions": {
3257 "typeIdentifier": "t_struct$_Info_$10_storage"
3258 }
3259 },
3260 {
3261 "id": 40,
3262 "nodeType": "ContractDefinition",
3263 "name": "Pool",
3264 "scope": 1,
3265 "src": "40:40:0",
3266 "nodes": [
3267 {
3268 "id": 41,
3269 "nodeType": "VariableDeclaration",
3270 "name": "obj",
3271 "scope": 40,
3272 "src": "50:10:0",
3273 "typeDescriptions": {
3274 "typeString": "struct Info",
3275 "typeIdentifier": "t_struct$_Info_$10_storage"
3276 }
3277 },
3278 {
3279 "id": 50,
3280 "nodeType": "FunctionDefinition",
3281 "name": "run",
3282 "scope": 40,
3283 "src": "60:20:0",
3284 "body": {
3285 "id": 60,
3286 "nodeType": "Block",
3287 "scope": 50,
3288 "src": "60:20:0"
3289 }
3290 }
3291 ]
3292 }
3293 ]
3294 }
3295 }
3296 });
3297 let cache = build_completion_cache(&sources, None, None);
3298 let source = format!("{:80}", "obj.start");
3300 let labels = labels_of(handle_completion(
3301 Some(&cache),
3302 &source,
3303 Position {
3304 line: 0,
3305 character: "obj.start".len() as u32,
3306 },
3307 None,
3308 Some(FileId(0)),
3309 ));
3310 assert!(
3311 labels.iter().any(|label| label == "startAt"),
3312 "fields missing: {labels:?}"
3313 );
3314 }
3315
3316 #[test]
3317 fn top_level_importables_keep_multiple_declarations_for_same_name() {
3318 let sources = json!({
3319 "/tmp/A.sol": {
3320 "id": 0,
3321 "ast": {
3322 "id": 1,
3323 "nodeType": "SourceUnit",
3324 "src": "0:100:0",
3325 "nodes": [
3326 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
3327 ]
3328 }
3329 },
3330 "/tmp/B.sol": {
3331 "id": 1,
3332 "ast": {
3333 "id": 2,
3334 "nodeType": "SourceUnit",
3335 "src": "0:100:1",
3336 "nodes": [
3337 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
3338 ]
3339 }
3340 }
3341 });
3342
3343 let cache = build_completion_cache(&sources, None, None);
3344 let entries = cache.top_level_importables_by_name.get("dup").unwrap();
3345 assert_eq!(entries.len(), 2);
3346 }
3347
3348 #[test]
3349 fn extract_top_level_importables_for_file_finds_expected_symbols() {
3350 let ast = json!({
3351 "id": 1,
3352 "nodeType": "SourceUnit",
3353 "src": "0:100:0",
3354 "nodes": [
3355 { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
3356 { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
3357 { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
3358 ]
3359 });
3360
3361 let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
3362 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
3363 assert!(names.contains(&"f"));
3364 assert!(names.contains(&"K"));
3365 assert!(!names.contains(&"V"));
3366 }
3367
3368 #[test]
3369 fn top_level_importables_can_be_replaced_per_file() {
3370 let sources = json!({
3371 "/tmp/A.sol": {
3372 "id": 0,
3373 "ast": {
3374 "id": 1,
3375 "nodeType": "SourceUnit",
3376 "src": "0:100:0",
3377 "nodes": [
3378 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
3379 ]
3380 }
3381 },
3382 "/tmp/B.sol": {
3383 "id": 1,
3384 "ast": {
3385 "id": 2,
3386 "nodeType": "SourceUnit",
3387 "src": "0:100:1",
3388 "nodes": [
3389 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
3390 ]
3391 }
3392 }
3393 });
3394
3395 let mut cache = build_completion_cache(&sources, None, None);
3396 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
3397
3398 cache.replace_top_level_importables_for_path(
3399 "/tmp/A.sol".to_string(),
3400 vec![TopLevelImportable {
3401 name: "newA".to_string(),
3402 declaring_path: "/tmp/A.sol".to_string(),
3403 node_type: "FunctionDefinition".to_string(),
3404 kind: CompletionItemKind::FUNCTION,
3405 }],
3406 );
3407 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
3408 assert!(cache.top_level_importables_by_name.contains_key("newA"));
3409
3410 cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
3411 assert!(!cache.top_level_importables_by_name.contains_key("newA"));
3412 }
3413
3414 #[test]
3415 fn append_auto_import_candidates_last_sets_tail_sort_text() {
3416 let base = vec![CompletionItem {
3417 label: "localVar".to_string(),
3418 ..Default::default()
3419 }];
3420 let auto = vec![CompletionItem {
3421 label: "ImportMe".to_string(),
3422 ..Default::default()
3423 }];
3424
3425 let out = append_auto_import_candidates_last(base, auto);
3426 assert_eq!(out.len(), 2);
3427 assert_eq!(out[1].label, "ImportMe");
3428 assert!(
3429 out[1]
3430 .sort_text
3431 .as_deref()
3432 .is_some_and(|s| s.starts_with("zz_autoimport_"))
3433 );
3434 }
3435
3436 #[test]
3437 fn append_auto_import_candidates_last_keeps_same_label_candidates() {
3438 let base = vec![CompletionItem {
3439 label: "B".to_string(),
3440 ..Default::default()
3441 }];
3442 let auto = vec![
3443 CompletionItem {
3444 label: "B".to_string(),
3445 detail: Some("ContractDefinition (./B.sol)".to_string()),
3446 ..Default::default()
3447 },
3448 CompletionItem {
3449 label: "B".to_string(),
3450 detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
3451 ..Default::default()
3452 },
3453 ];
3454
3455 let out = append_auto_import_candidates_last(base, auto);
3456 assert_eq!(out.len(), 3);
3457 }
3458
3459 #[test]
3460 fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
3461 let base = vec![CompletionItem {
3462 label: "B".to_string(),
3463 ..Default::default()
3464 }];
3465 let auto = vec![CompletionItem {
3466 label: "B".to_string(),
3467 additional_text_edits: Some(vec![TextEdit {
3468 range: Range {
3469 start: Position {
3470 line: 0,
3471 character: 0,
3472 },
3473 end: Position {
3474 line: 0,
3475 character: 0,
3476 },
3477 },
3478 new_text: "import {B} from \"./B.sol\";\n".to_string(),
3479 }]),
3480 ..Default::default()
3481 }];
3482 let out = append_auto_import_candidates_last(base, auto);
3483 assert!(
3484 out[0].additional_text_edits.is_some(),
3485 "base item should inherit unique import edit"
3486 );
3487 }
3488
3489 #[test]
3490 fn top_level_importable_candidates_include_import_edit() {
3491 let mut cache = empty_cache();
3492 cache.top_level_importables_by_name.insert(
3493 SymbolName::new("B"),
3494 vec![TopLevelImportable {
3495 name: "B".to_string(),
3496 declaring_path: "/tmp/example/B.sol".to_string(),
3497 node_type: "ContractDefinition".to_string(),
3498 kind: CompletionItemKind::CLASS,
3499 }],
3500 );
3501
3502 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
3503 let items = super::top_level_importable_completion_candidates(
3504 &cache,
3505 Some("/tmp/example/A.sol"),
3506 source,
3507 );
3508 assert_eq!(items.len(), 1);
3509 let edit_text = items[0]
3510 .additional_text_edits
3511 .as_ref()
3512 .and_then(|edits| edits.first())
3513 .map(|e| e.new_text.clone())
3514 .unwrap_or_default();
3515 assert!(edit_text.contains("import {B} from \"./B.sol\";"));
3516 }
3517
3518 #[test]
3519 fn handle_completion_general_path_keeps_base_items() {
3520 let mut cache = empty_cache();
3521 cache.general_completions = vec![CompletionItem {
3522 label: "A".to_string(),
3523 ..Default::default()
3524 }];
3525
3526 let resp = super::handle_completion(
3527 Some(&cache),
3528 "contract X {}",
3529 Position {
3530 line: 0,
3531 character: 0,
3532 },
3533 None,
3534 None,
3535 );
3536 match resp {
3537 Some(CompletionResponse::List(list)) => {
3538 assert_eq!(list.items.len(), 1);
3539 assert_eq!(list.items[0].label, "A");
3540 }
3541 _ => panic!("expected completion list"),
3542 }
3543 }
3544
3545 #[test]
3548 fn make_import_item_has_filter_text_and_text_edit() {
3549 let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
3550 assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
3551 assert!(item.text_edit.is_some(), "text_edit should be set");
3552 assert!(
3553 item.insert_text.is_none(),
3554 "insert_text should be absent when text_edit is set"
3555 );
3556 }
3557
3558 #[test]
3559 fn all_sol_import_paths_no_panic_missing_dir() {
3560 let current = std::path::Path::new("/nonexistent/src/Foo.sol");
3561 let root = std::path::Path::new("/nonexistent");
3562 let items = super::all_sol_import_paths(current, root, &[], None);
3563 assert!(items.is_empty());
3564 }
3565
3566 #[test]
3567 fn all_sol_import_paths_relative_labels() {
3568 let root = std::path::Path::new("example");
3570 let current = root.join("A.sol");
3571 let items = super::all_sol_import_paths(¤t, root, &[], None);
3572 for item in &items {
3574 assert!(
3575 item.label.starts_with("./") || item.label.starts_with("../"),
3576 "label should be relative: {}",
3577 item.label
3578 );
3579 assert!(
3580 item.label.ends_with(".sol"),
3581 "label should end with .sol: {}",
3582 item.label
3583 );
3584 }
3585 assert!(!items.is_empty(), "should find at least one .sol file");
3586 }
3587
3588 fn nested_enum_sources() -> serde_json::Value {
3592 let ast: serde_json::Value = serde_json::from_str(NESTED_ENUM_AST).unwrap();
3593 json!({ "E.sol": { "id": 0, "ast": ast } })
3594 }
3595
3596 const NESTED_ENUM_AST: &str = r#"{"absolutePath":"E.sol","exportedSymbols":{"C":[18],"RafluxTypes":[6]},"id":19,"license":"MIT","nodeType":"SourceUnit","nodes":[{"id":1,"literals":["solidity","^","0.8",".0"],"nodeType":"PragmaDirective","src":"32:23:0"},{"abstract":false,"baseContracts":[],"canonicalName":"RafluxTypes","contractDependencies":[],"contractKind":"library","fullyImplemented":true,"id":6,"linearizedBaseContracts":[6],"name":"RafluxTypes","nameLocation":"64:11:0","nodeType":"ContractDefinition","nodes":[{"canonicalName":"RafluxTypes.ListingStatus","id":5,"members":[{"id":2,"name":"NONE","nameLocation":"103:4:0","nodeType":"EnumValue","src":"103:4:0"},{"id":3,"name":"ACTIVE","nameLocation":"109:6:0","nodeType":"EnumValue","src":"109:6:0"},{"id":4,"name":"SETTLED","nameLocation":"117:7:0","nodeType":"EnumValue","src":"117:7:0"}],"name":"ListingStatus","nameLocation":"87:13:0","nodeType":"EnumDefinition","src":"82:44:0"}],"scope":19,"src":"56:72:0","usedErrors":[],"usedEvents":[]},{"abstract":false,"baseContracts":[],"canonicalName":"C","contractDependencies":[],"contractKind":"contract","fullyImplemented":true,"id":18,"linearizedBaseContracts":[18],"name":"C","nameLocation":"138:1:0","nodeType":"ContractDefinition","nodes":[{"body":{"id":16,"nodeType":"Block","src":"209:56:0","statements":[{"expression":{"expression":{"expression":{"id":12,"name":"RafluxTypes","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":6,"src":"226:11:0","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_RafluxTypes_$6_$","typeString":"type(library RafluxTypes)"}},"id":13,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"238:13:0","memberName":"ListingStatus","nodeType":"MemberAccess","referencedDeclaration":5,"src":"226:25:0","typeDescriptions":{"typeIdentifier":"t_type$_t_enum$_ListingStatus_$5_$","typeString":"type(enum RafluxTypes.ListingStatus)"}},"id":14,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"memberLocation":"252:6:0","memberName":"ACTIVE","nodeType":"MemberAccess","referencedDeclaration":3,"src":"226:32:0","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"}},"functionReturnParameters":11,"id":15,"nodeType":"Return","src":"219:39:0"}]},"functionSelector":"26121ff0","id":17,"implemented":true,"kind":"function","modifiers":[],"name":"f","nameLocation":"155:1:0","nodeType":"FunctionDefinition","parameters":{"id":7,"nodeType":"ParameterList","parameters":[],"src":"156:2:0"},"returnParameters":{"id":11,"nodeType":"ParameterList","parameters":[{"constant":false,"id":10,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":17,"src":"182:25:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"},"typeName":{"id":9,"nodeType":"UserDefinedTypeName","pathNode":{"id":8,"name":"RafluxTypes.ListingStatus","nameLocations":["182:11:0","194:13:0"],"nodeType":"IdentifierPath","referencedDeclaration":5,"src":"182:25:0"},"referencedDeclaration":5,"src":"182:25:0","typeDescriptions":{"typeIdentifier":"t_enum$_ListingStatus_$5","typeString":"enum RafluxTypes.ListingStatus"}},"visibility":"internal"}],"src":"181:27:0"},"scope":18,"src":"146:119:0","stateMutability":"pure","virtual":false,"visibility":"external"}],"scope":19,"src":"129:138:0","usedErrors":[],"usedEvents":[]}],"src":"32:236:0"}"#;
3597
3598 #[test]
3599 fn nested_enum_chain_completes_enum_values() {
3600 let cache = build_completion_cache(&nested_enum_sources(), None, None);
3601 let chain = crate::completion::parse_dot_chain(" RafluxTypes.ListingStatus.", 34);
3602 let labels: Vec<String> = crate::completion::get_chain_completions(&cache, &chain, None)
3603 .into_iter()
3604 .map(|c| c.label)
3605 .collect();
3606 assert_eq!(labels, vec!["NONE", "ACTIVE", "SETTLED"]);
3607 }
3608}