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