1use crate::annotation_discovery::AnnotationDiscovery;
6use crate::document::DocumentManager;
7use crate::module_cache::ModuleCache;
8use crate::type_inference::infer_variable_type;
9use crate::util::{get_word_at_position, offset_to_line_col, position_to_offset};
10use shape_ast::ast::{
11 ImportItems, Item, Program, Span, Statement, TraitMember, TraitMemberSignature, TypeName,
12};
13use shape_ast::parser::parse_program;
14use std::path::{Path, PathBuf};
15use tower_lsp_server::ls_types::{
16 DocumentHighlight, DocumentHighlightKind, GotoDefinitionResponse, Location, Position, Range,
17 Uri,
18};
19
20pub fn get_definition(
25 text: &str,
26 position: Position,
27 uri: &Uri,
28 module_cache: Option<&ModuleCache>,
29 annotation_discovery: Option<&AnnotationDiscovery>,
30 cached_program: Option<&Program>,
31) -> Option<GotoDefinitionResponse> {
32 let word = get_word_at_position(text, position)?;
34
35 let program = match parse_program(text) {
37 Ok(p) => p,
38 Err(_) => {
39 if let Some(cached) = cached_program {
40 cached.clone()
41 } else {
42 let partial = shape_ast::parser::resilient::parse_program_resilient(text);
44 if partial.items.is_empty() {
45 return None;
46 }
47 partial.into_program()
48 }
49 }
50 };
51
52 if let Some(location) = find_definition_location(&program, &word, uri, text) {
54 return Some(GotoDefinitionResponse::Scalar(location));
55 }
56
57 if let Some(cache) = module_cache {
59 if let Some(location) = find_imported_definition(&program, &word, uri, cache) {
60 return Some(GotoDefinitionResponse::Scalar(location));
61 }
62 }
63
64 if let Some(discovery) = annotation_discovery {
66 if let Some(location) = find_annotation_definition(&word, discovery, uri) {
67 return Some(GotoDefinitionResponse::Scalar(location));
68 }
69 }
70
71 None
72}
73
74pub fn get_references(text: &str, position: Position, uri: &Uri) -> Option<Vec<Location>> {
79 get_references_with_fallback(text, position, uri, None)
80}
81
82pub fn get_references_cross_file(
99 text: &str,
100 position: Position,
101 uri: &Uri,
102 cached_program: Option<&Program>,
103 documents: Option<&DocumentManager>,
104 module_cache: Option<&ModuleCache>,
105 workspace_root: Option<&Path>,
106) -> Option<Vec<Location>> {
107 let mut locations = get_references_with_fallback(text, position, uri, cached_program)
109 .unwrap_or_default();
110
111 let Some(word) = get_word_at_position(text, position) else {
113 return if locations.is_empty() {
114 None
115 } else {
116 Some(locations)
117 };
118 };
119
120 let program = match parse_program(text) {
122 Ok(p) => p,
123 Err(_) => match cached_program {
124 Some(p) => p.clone(),
125 None => {
126 return if locations.is_empty() {
127 None
128 } else {
129 Some(locations)
130 };
131 }
132 },
133 };
134
135 if !is_module_scope_symbol(&program, &word) {
136 return if locations.is_empty() {
139 None
140 } else {
141 Some(locations)
142 };
143 }
144
145 let mut visited: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
148 if let Some(current_path) = uri.to_file_path() {
149 visited.insert(current_path.into_owned());
150 }
151
152 if let Some(docs) = documents {
153 for other_uri in docs.all_uris() {
154 if &other_uri == uri {
155 continue;
156 }
157 let Some(other_path_cow) = other_uri.to_file_path() else {
158 continue;
159 };
160 let other_path = other_path_cow.into_owned();
161 if !visited.insert(other_path.clone()) {
162 continue;
163 }
164 let Some(other_doc) = docs.get(&other_uri) else {
165 continue;
166 };
167 let other_text = other_doc.text();
168 collect_module_scope_refs_in_file(
169 &other_text,
170 &other_uri,
171 &word,
172 &mut locations,
173 );
174 }
175 }
176
177 if let (Some(cache), Some(root)) = (module_cache, workspace_root) {
178 let _ = cache; for path in cache.enumerate_workspace_shape_files(root) {
180 if !visited.insert(path.clone()) {
181 continue;
182 }
183 let Some(other_uri) = Uri::from_file_path(&path) else {
184 continue;
185 };
186 let Ok(other_text) = std::fs::read_to_string(&path) else {
188 continue;
189 };
190 collect_module_scope_refs_in_file(
191 &other_text,
192 &other_uri,
193 &word,
194 &mut locations,
195 );
196 }
197 }
198
199 if locations.is_empty() {
200 None
201 } else {
202 Some(locations)
203 }
204}
205
206fn is_module_scope_symbol(program: &Program, name: &str) -> bool {
215 for item in &program.items {
216 match item {
217 Item::Function(func, _) if func.name == name => return true,
218 Item::ForeignFunction(func, _) if func.name == name => return true,
219 Item::Trait(t, _) if t.name == name => return true,
220 Item::Enum(e, _) if e.name == name => return true,
221 Item::TypeAlias(ta, _) if ta.name == name => return true,
222 Item::StructType(s, _) if s.name == name => return true,
223 Item::VariableDecl(decl, _) => {
224 for (n, _) in crate::symbols::get_pattern_names(&decl.pattern) {
225 if n == name {
226 return true;
227 }
228 }
229 }
230 Item::Statement(Statement::VariableDecl(decl, _), _) => {
231 for (n, _) in crate::symbols::get_pattern_names(&decl.pattern) {
232 if n == name {
233 return true;
234 }
235 }
236 }
237 Item::Import(import_stmt, _) => match &import_stmt.items {
238 ImportItems::Named(specs) => {
239 for spec in specs {
240 let local = spec.alias.as_ref().unwrap_or(&spec.name);
241 if local == name {
242 return true;
243 }
244 }
245 }
246 ImportItems::Namespace { name: ns_name, alias } => {
247 let local = alias.as_ref().unwrap_or(ns_name);
248 if local == name {
249 return true;
250 }
251 }
252 },
253 _ => {}
254 }
255 }
256 false
257}
258
259fn collect_module_scope_refs_in_file(
264 text: &str,
265 uri: &Uri,
266 name: &str,
267 out: &mut Vec<Location>,
268) {
269 let program = match parse_program(text) {
270 Ok(p) => p,
271 Err(_) => {
272 let partial = shape_ast::parse_program_resilient(text);
273 if partial.items.is_empty() {
274 return;
275 }
276 partial.into_program()
277 }
278 };
279
280 if !is_module_scope_symbol(&program, name) {
281 return;
282 }
283
284 let tree = crate::scope::ScopeTree::build(&program, text);
285 let Some(root) = tree.scopes.first() else {
289 return;
290 };
291 for binding in &root.bindings {
292 if binding.name != name {
293 continue;
294 }
295 let push = |span: (usize, usize), out: &mut Vec<Location>| {
296 let (sl, sc) = offset_to_line_col(text, span.0);
297 let (el, ec) = offset_to_line_col(text, span.1);
298 out.push(Location {
299 uri: uri.clone(),
300 range: Range {
301 start: Position {
302 line: sl,
303 character: sc,
304 },
305 end: Position {
306 line: el,
307 character: ec,
308 },
309 },
310 });
311 };
312 push(binding.def_span, out);
313 for span in &binding.references {
314 push(*span, out);
315 }
316 }
317}
318
319pub fn get_references_with_fallback(
321 text: &str,
322 position: Position,
323 uri: &Uri,
324 cached_program: Option<&Program>,
325) -> Option<Vec<Location>> {
326 let offset = position_to_offset(text, position)?;
328
329 let program = match parse_program(text) {
331 Ok(p) => p,
332 Err(_) => {
333 if let Some(cached) = cached_program {
334 cached.clone()
335 } else {
336 let partial = shape_ast::parse_program_resilient(text);
337 if partial.items.is_empty() {
338 return None;
339 }
340 partial.into_program()
341 }
342 }
343 };
344 let tree = crate::scope::ScopeTree::build(&program, text);
345
346 let spans = tree.references_of(offset)?;
348
349 let locations: Vec<Location> = spans
350 .into_iter()
351 .map(|(start, end)| {
352 let (start_line, start_col) = offset_to_line_col(text, start);
353 let (end_line, end_col) = offset_to_line_col(text, end);
354 Location {
355 uri: uri.clone(),
356 range: Range {
357 start: Position {
358 line: start_line,
359 character: start_col,
360 },
361 end: Position {
362 line: end_line,
363 character: end_col,
364 },
365 },
366 }
367 })
368 .collect();
369
370 if locations.is_empty() {
371 let word = get_word_at_position(text, position)?;
373 let fallback = find_all_references(&program, &word, uri, text);
374 if fallback.is_empty() {
375 None
376 } else {
377 Some(fallback)
378 }
379 } else {
380 Some(locations)
381 }
382}
383
384pub fn get_type_definition(
403 text: &str,
404 position: Position,
405 uri: &Uri,
406 module_cache: Option<&ModuleCache>,
407 cached_program: Option<&Program>,
408) -> Option<GotoDefinitionResponse> {
409 let word = get_word_at_position(text, position)?;
410
411 let program = match parse_program(text) {
412 Ok(p) => p,
413 Err(_) => {
414 if let Some(cached) = cached_program {
415 cached.clone()
416 } else {
417 let partial = shape_ast::parser::resilient::parse_program_resilient(text);
418 if partial.items.is_empty() {
419 return None;
420 }
421 partial.into_program()
422 }
423 }
424 };
425
426 let inferred = infer_variable_type(&program, &word)?;
427 let base = extract_base_type_name(&inferred)?;
428 if is_builtin_primitive(&base) {
429 return None;
430 }
431
432 if let Some(location) = find_definition_location(&program, &base, uri, text) {
433 return Some(GotoDefinitionResponse::Scalar(location));
434 }
435
436 if let Some(cache) = module_cache {
437 if let Some(location) = find_imported_definition(&program, &base, uri, cache) {
438 return Some(GotoDefinitionResponse::Scalar(location));
439 }
440 }
441
442 None
443}
444
445pub fn get_implementations(
462 text: &str,
463 position: Position,
464 uri: &Uri,
465 cached_program: Option<&Program>,
466) -> Option<Vec<Location>> {
467 let word = get_word_at_position(text, position)?;
468
469 let program = match parse_program(text) {
470 Ok(p) => p,
471 Err(_) => {
472 if let Some(cached) = cached_program {
473 cached.clone()
474 } else {
475 let partial = shape_ast::parser::resilient::parse_program_resilient(text);
476 if partial.items.is_empty() {
477 return None;
478 }
479 partial.into_program()
480 }
481 }
482 };
483
484 if let Some(cursor_offset) = position_to_offset(text, position) {
488 if let Some((trait_name, method_name)) =
489 find_trait_method_at_offset(&program, cursor_offset, &word)
490 {
491 let impl_method_locations =
492 collect_impl_method_locations(&program, trait_name, method_name, uri, text);
493 if !impl_method_locations.is_empty() {
494 return Some(impl_method_locations);
495 }
496 }
497 }
498
499 let mut locations: Vec<Location> = Vec::new();
500
501 for item in &program.items {
502 match item {
503 Item::Impl(impl_block, item_span) => {
504 let trait_str = type_name_str(&impl_block.trait_name);
505 let target_str = type_name_str(&impl_block.target_type);
506 if trait_str == word || target_str == word {
507 locations.push(create_location_from_span(uri, *item_span, text));
508 }
509 }
510 Item::Extend(extend_stmt, item_span) => {
511 let target_str = type_name_str(&extend_stmt.type_name);
512 if target_str == word {
513 locations.push(create_location_from_span(uri, *item_span, text));
514 }
515 }
516 _ => {}
517 }
518 }
519
520 if locations.is_empty() {
521 None
522 } else {
523 Some(locations)
524 }
525}
526
527fn find_trait_method_at_offset<'p>(
537 program: &'p Program,
538 offset: usize,
539 word: &str,
540) -> Option<(&'p str, &'p str)> {
541 for item in &program.items {
542 if let Item::Trait(trait_def, _trait_span) = item {
543 for member in &trait_def.members {
544 match member {
545 TraitMember::Required(TraitMemberSignature::Method { name, span, .. }) => {
546 if offset >= span.start && offset <= span.end && name == word {
547 return Some((trait_def.name.as_str(), name.as_str()));
548 }
549 }
550 TraitMember::Default(method) => {
551 let span = method.span;
552 if offset >= span.start && offset <= span.end && method.name == word {
553 return Some((trait_def.name.as_str(), method.name.as_str()));
554 }
555 }
556 _ => {}
557 }
558 }
559 }
560 }
561 None
562}
563
564fn collect_impl_method_locations(
568 program: &Program,
569 trait_name: &str,
570 method_name: &str,
571 uri: &Uri,
572 text: &str,
573) -> Vec<Location> {
574 let mut locations: Vec<Location> = Vec::new();
575 for item in &program.items {
576 if let Item::Impl(impl_block, _) = item {
577 if type_name_str(&impl_block.trait_name) != trait_name {
578 continue;
579 }
580 for method in &impl_block.methods {
581 if method.name == method_name {
582 locations.push(create_location_from_span(uri, method.span, text));
583 }
584 }
585 }
586 }
587 locations
588}
589
590pub fn get_declaration(
598 text: &str,
599 position: Position,
600 uri: &Uri,
601 module_cache: Option<&ModuleCache>,
602 annotation_discovery: Option<&AnnotationDiscovery>,
603 cached_program: Option<&Program>,
604) -> Option<GotoDefinitionResponse> {
605 get_definition(
606 text,
607 position,
608 uri,
609 module_cache,
610 annotation_discovery,
611 cached_program,
612 )
613}
614
615pub fn get_document_highlights(
626 text: &str,
627 position: Position,
628 cached_program: Option<&Program>,
629) -> Option<Vec<DocumentHighlight>> {
630 let offset = position_to_offset(text, position)?;
631
632 let program = match parse_program(text) {
633 Ok(p) => p,
634 Err(_) => {
635 if let Some(cached) = cached_program {
636 cached.clone()
637 } else {
638 let partial = shape_ast::parse_program_resilient(text);
639 if partial.items.is_empty() {
640 return None;
641 }
642 partial.into_program()
643 }
644 }
645 };
646
647 let tree = crate::scope::ScopeTree::build(&program, text);
648
649 let spans = tree.references_of(offset);
650
651 let highlights: Vec<DocumentHighlight> = match spans {
652 Some(spans) => spans
653 .into_iter()
654 .map(|(start, end)| span_to_highlight(text, start, end))
655 .collect(),
656 None => {
657 let word = get_word_at_position(text, position)?;
659 text_search_highlights(text, &word)
660 }
661 };
662
663 if highlights.is_empty() {
664 None
665 } else {
666 Some(highlights)
667 }
668}
669
670fn text_search_highlights(text: &str, symbol_name: &str) -> Vec<DocumentHighlight> {
674 let mut highlights = Vec::new();
675 let lines: Vec<&str> = text.lines().collect();
676 for (line_idx, line) in lines.iter().enumerate() {
677 let mut char_pos = 0;
678 while let Some(pos) = line[char_pos..].find(symbol_name) {
679 let absolute_pos = char_pos + pos;
680 let is_start_boundary = absolute_pos == 0
681 || !line
682 .chars()
683 .nth(absolute_pos - 1)
684 .map(|c| c.is_alphanumeric() || c == '_')
685 .unwrap_or(false);
686 let is_end_boundary = absolute_pos + symbol_name.len() >= line.len()
687 || !line
688 .chars()
689 .nth(absolute_pos + symbol_name.len())
690 .map(|c| c.is_alphanumeric() || c == '_')
691 .unwrap_or(false);
692
693 if is_start_boundary && is_end_boundary {
694 highlights.push(DocumentHighlight {
695 range: Range {
696 start: Position {
697 line: line_idx as u32,
698 character: absolute_pos as u32,
699 },
700 end: Position {
701 line: line_idx as u32,
702 character: (absolute_pos + symbol_name.len()) as u32,
703 },
704 },
705 kind: Some(DocumentHighlightKind::TEXT),
706 });
707 }
708 char_pos = absolute_pos + symbol_name.len();
709 }
710 }
711 highlights
712}
713
714fn span_to_highlight(text: &str, start: usize, end: usize) -> DocumentHighlight {
715 let (start_line, start_col) = offset_to_line_col(text, start);
716 let (end_line, end_col) = offset_to_line_col(text, end);
717 DocumentHighlight {
718 range: Range {
719 start: Position {
720 line: start_line,
721 character: start_col,
722 },
723 end: Position {
724 line: end_line,
725 character: end_col,
726 },
727 },
728 kind: Some(DocumentHighlightKind::TEXT),
729 }
730}
731
732fn type_name_str(type_name: &TypeName) -> &str {
734 match type_name {
735 TypeName::Simple(n) => n.as_str(),
736 TypeName::Generic { name, .. } => name.as_str(),
737 }
738}
739
740fn extract_base_type_name(rendered: &str) -> Option<String> {
750 let mut current: String = rendered.trim().to_string();
751 if current.is_empty() || current.starts_with('{') {
752 return None;
753 }
754
755 loop {
757 let trimmed = current.trim_start();
758 if let Some(rest) = trimmed.strip_prefix("&mut ") {
759 current = rest.trim_start().to_string();
760 } else if let Some(rest) = trimmed.strip_prefix('&') {
761 current = rest.trim_start().to_string();
762 } else {
763 break;
764 }
765 }
766
767 while let Some(rest) = current.strip_suffix('?') {
769 current = rest.trim_end().to_string();
770 }
771
772 loop {
775 let unwrapped: Option<String> = if let Some(inner) = strip_generic_wrapper(¤t, "Array")
776 {
777 Some(inner.to_string())
778 } else if let Some(inner) = strip_generic_wrapper(¤t, "Option") {
779 Some(inner.to_string())
780 } else if let Some(inner) = strip_generic_wrapper(¤t, "Result") {
781 Some(first_generic_arg(inner).to_string())
783 } else if let Some(inner) = strip_generic_wrapper(¤t, "HashMap") {
784 Some(first_generic_arg(inner).to_string())
786 } else {
787 None
788 };
789 match unwrapped {
790 Some(inner) => {
791 let inner_trim = inner.trim().to_string();
792 if inner_trim == current {
793 break;
794 }
795 current = inner_trim;
796 }
797 None => break,
798 }
799 }
800
801 let base = current
803 .split(|c: char| c == '<' || c == ',' || c.is_whitespace())
804 .next()?;
805 let base = base.trim();
806 if base.is_empty() {
807 None
808 } else {
809 Some(base.to_string())
810 }
811}
812
813fn strip_generic_wrapper<'a>(s: &'a str, name: &str) -> Option<&'a str> {
815 let s = s.strip_prefix(name)?.trim_start();
816 let s = s.strip_prefix('<')?;
817 let s = s.strip_suffix('>')?;
818 Some(s)
819}
820
821fn first_generic_arg(args: &str) -> &str {
822 args.split(',').next().unwrap_or(args).trim()
823}
824
825fn is_builtin_primitive(name: &str) -> bool {
827 matches!(
828 name,
829 "int"
830 | "number"
831 | "bool"
832 | "string"
833 | "decimal"
834 | "bigint"
835 | "unit"
836 | "null"
837 | "DateTime"
838 | "unknown"
839 | "any"
840 )
841}
842
843fn find_definition_location(
845 program: &Program,
846 symbol_name: &str,
847 uri: &Uri,
848 text: &str,
849) -> Option<Location> {
850 for item in &program.items {
851 match item {
852 Item::Function(func, _) if func.name == symbol_name => {
853 return Some(create_location_from_span(uri, func.name_span, text));
854 }
855 Item::VariableDecl(var_decl, item_span) => {
856 for (name, name_span) in crate::symbols::get_pattern_names(&var_decl.pattern) {
857 if name == symbol_name {
858 let span = if name_span.is_dummy() {
859 *item_span
860 } else {
861 name_span
862 };
863 return Some(create_location_from_span(uri, span, text));
864 }
865 }
866 }
867 Item::Statement(Statement::VariableDecl(var_decl, stmt_span), _) => {
868 for (name, name_span) in crate::symbols::get_pattern_names(&var_decl.pattern) {
869 if name == symbol_name {
870 let span = if name_span.is_dummy() {
871 *stmt_span
872 } else {
873 name_span
874 };
875 return Some(create_location_from_span(uri, span, text));
876 }
877 }
878 }
879 Item::TypeAlias(type_alias, item_span) if type_alias.name == symbol_name => {
880 return Some(create_location_from_span(uri, *item_span, text));
881 }
882 Item::Enum(enum_def, item_span) if enum_def.name == symbol_name => {
883 return Some(create_location_from_span(uri, *item_span, text));
884 }
885 Item::Trait(trait_def, item_span) if trait_def.name == symbol_name => {
886 return Some(create_location_from_span(uri, *item_span, text));
887 }
888 Item::Impl(impl_block, _) => {
889 let trait_name_str = match &impl_block.trait_name {
891 shape_ast::ast::TypeName::Simple(n) => n.as_str(),
892 shape_ast::ast::TypeName::Generic { name, .. } => name.as_str(),
893 };
894 if trait_name_str == symbol_name {
897 for other_item in &program.items {
899 if let Item::Trait(td, ts) = other_item {
900 if td.name == symbol_name {
901 return Some(create_location_from_span(uri, *ts, text));
902 }
903 }
904 }
905 }
906 for method in &impl_block.methods {
909 if method.name == symbol_name {
910 for other_item in &program.items {
912 if let Item::Trait(td, ts) = other_item {
913 if td.name == trait_name_str {
914 return Some(create_location_from_span(uri, *ts, text));
916 }
917 }
918 }
919 }
920 }
921 }
922 Item::Extend(extend_stmt, item_span) => {
923 for method in &extend_stmt.methods {
925 if method.name == symbol_name {
926 return Some(create_location_from_span(uri, *item_span, text));
927 }
928 }
929 }
930 Item::StructType(struct_def, item_span) if struct_def.name == symbol_name => {
931 return Some(create_location_from_span(uri, *item_span, text));
932 }
933 _ => {}
934 }
935 }
936
937 if symbol_name == "format" || symbol_name == "toString" {
940 for item in &program.items {
942 if let Item::Impl(impl_block, item_span) = item {
943 let trait_name_str = match &impl_block.trait_name {
944 shape_ast::ast::TypeName::Simple(n) => n.as_str(),
945 shape_ast::ast::TypeName::Generic { name, .. } => name.as_str(),
946 };
947 if trait_name_str == "Display" {
948 for method in &impl_block.methods {
950 if method.name == symbol_name {
951 return Some(create_location_from_span(uri, *item_span, text));
952 }
953 }
954 return Some(create_location_from_span(uri, *item_span, text));
956 }
957 }
958 }
959 }
960
961 None
962}
963
964fn find_all_references(
966 _program: &Program,
967 symbol_name: &str,
968 uri: &Uri,
969 text: &str,
970) -> Vec<Location> {
971 let mut locations = Vec::new();
972 let lines: Vec<&str> = text.lines().collect();
973
974 for (line_idx, line) in lines.iter().enumerate() {
977 let mut char_pos = 0;
978 while let Some(pos) = line[char_pos..].find(symbol_name) {
979 let absolute_pos = char_pos + pos;
980
981 let is_start_boundary = absolute_pos == 0
983 || !line
984 .chars()
985 .nth(absolute_pos - 1)
986 .map(|c| c.is_alphanumeric() || c == '_')
987 .unwrap_or(false);
988
989 let is_end_boundary = absolute_pos + symbol_name.len() >= line.len()
990 || !line
991 .chars()
992 .nth(absolute_pos + symbol_name.len())
993 .map(|c| c.is_alphanumeric() || c == '_')
994 .unwrap_or(false);
995
996 if is_start_boundary && is_end_boundary {
997 locations.push(Location {
998 uri: uri.clone(),
999 range: Range {
1000 start: Position {
1001 line: line_idx as u32,
1002 character: absolute_pos as u32,
1003 },
1004 end: Position {
1005 line: line_idx as u32,
1006 character: (absolute_pos + symbol_name.len()) as u32,
1007 },
1008 },
1009 });
1010 }
1011
1012 char_pos = absolute_pos + symbol_name.len();
1013 }
1014 }
1015
1016 locations
1017}
1018
1019fn create_location_from_span(uri: &Uri, span: Span, text: &str) -> Location {
1021 let (start_line, start_col) = offset_to_line_col(text, span.start);
1022 let (end_line, end_col) = offset_to_line_col(text, span.end);
1023
1024 Location {
1025 uri: uri.clone(),
1026 range: Range {
1027 start: Position {
1028 line: start_line,
1029 character: start_col,
1030 },
1031 end: Position {
1032 line: end_line,
1033 character: end_col,
1034 },
1035 },
1036 }
1037}
1038
1039fn find_imported_definition(
1041 program: &Program,
1042 symbol_name: &str,
1043 current_uri: &Uri,
1044 module_cache: &ModuleCache,
1045) -> Option<Location> {
1046 let current_path = current_uri.to_file_path()?.into_owned();
1048
1049 for item in &program.items {
1051 if let Item::Import(import_stmt, _span) = item {
1052 let imports_symbol = match &import_stmt.items {
1054 ImportItems::Named(specs) => specs.iter().any(|spec| {
1055 let imported_name = spec.alias.as_ref().unwrap_or(&spec.name);
1056 imported_name == symbol_name
1057 }),
1058 ImportItems::Namespace { name, alias } => {
1059 let local_name = alias.as_ref().unwrap_or(name);
1060 local_name == symbol_name
1061 }
1062 };
1063
1064 if !imports_symbol {
1065 continue;
1066 }
1067
1068 let resolved_path =
1072 module_cache.resolve_import(&import_stmt.from, ¤t_path, None)?;
1073
1074 let module_info =
1076 module_cache.load_module_with_context(&resolved_path, ¤t_path, None)?;
1077
1078 for export in &module_info.exports {
1080 if export.exported_name() == symbol_name {
1081 let target_uri = Uri::from_file_path(&module_info.path)?;
1083 let source = std::fs::read_to_string(&module_info.path).ok()?;
1084
1085 let location = find_definition_location(
1087 &module_info.program,
1088 &export.name,
1089 &target_uri,
1090 &source,
1091 )?;
1092
1093 return Some(location);
1094 }
1095 }
1096 }
1097 }
1098
1099 None
1100}
1101
1102fn find_annotation_definition(
1104 annotation_name: &str,
1105 annotation_discovery: &AnnotationDiscovery,
1106 current_uri: &Uri,
1107) -> Option<Location> {
1108 let info = annotation_discovery.get(annotation_name)?;
1110
1111 if info.location != Span::default() {
1113 let target_uri = if let Some(ref source_path) = info.source_file {
1115 Uri::from_file_path(source_path)?
1117 } else {
1118 current_uri.clone()
1120 };
1121
1122 let source = if let Some(ref source_path) = info.source_file {
1124 std::fs::read_to_string(source_path).ok()?
1125 } else {
1126 return Some(Location {
1129 uri: target_uri,
1130 range: Range {
1131 start: Position {
1132 line: 0,
1133 character: 0,
1134 },
1135 end: Position {
1136 line: 0,
1137 character: 0,
1138 },
1139 },
1140 });
1141 };
1142
1143 let (line, col) = offset_to_line_col(&source, info.location.start);
1144 Some(Location {
1145 uri: target_uri,
1146 range: Range {
1147 start: Position {
1148 line,
1149 character: col,
1150 },
1151 end: Position {
1152 line,
1153 character: col + annotation_name.len() as u32,
1154 },
1155 },
1156 })
1157 } else {
1158 None
1159 }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 #[test]
1167 fn test_get_word_at_position() {
1168 let text = "let myVar = 5;";
1169 let word = get_word_at_position(
1170 text,
1171 Position {
1172 line: 0,
1173 character: 5,
1174 },
1175 );
1176 assert_eq!(word, Some("myVar".to_string()));
1177 }
1178
1179 #[test]
1180 fn test_find_function_definition() {
1181 let code = r#"function myFunc(x, y) {
1182 return x + y;
1183}
1184
1185let result = myFunc(1, 2);
1186"#;
1187 let program = parse_program(code).unwrap();
1188 let uri = Uri::from_file_path("/test.shape").unwrap();
1189
1190 let location = find_definition_location(&program, "myFunc", &uri, code);
1191 assert!(location.is_some());
1192
1193 let loc = location.unwrap();
1194 assert_eq!(loc.range.start.line, 0);
1195 assert_eq!(loc.range.start.character, 9); }
1198
1199 #[test]
1200 fn test_find_variable_definition() {
1201 let code = r#"let myVar = 42;
1202let x = myVar + 5;
1203"#;
1204 let program = parse_program(code).unwrap();
1205 let uri = Uri::from_file_path("/test.shape").unwrap();
1206
1207 let location = find_definition_location(&program, "myVar", &uri, code);
1208 assert!(location.is_some());
1209 }
1210
1211 #[test]
1212 fn test_find_references() {
1213 let code = r#"let myVar = 42;
1214let x = myVar + 5;
1215let y = myVar * 2;
1216"#;
1217 let program = parse_program(code).unwrap();
1218 let uri = Uri::from_file_path("/test.shape").unwrap();
1219
1220 let refs = find_all_references(&program, "myVar", &uri, code);
1221 assert_eq!(refs.len(), 3); }
1223
1224 #[test]
1225 fn test_get_definition_with_module_cache() {
1226 let code = r#"function localFunc() {
1227 return 42;
1228}
1229"#;
1230 let uri = Uri::from_file_path("/test.shape").unwrap();
1231 let cache = ModuleCache::new();
1232
1233 let definition = get_definition(
1234 code,
1235 Position {
1236 line: 0,
1237 character: 10,
1238 },
1239 &uri,
1240 Some(&cache),
1241 None,
1242 None,
1243 );
1244 assert!(definition.is_some());
1245 }
1246
1247 #[test]
1248 fn test_find_imported_definition_not_found() {
1249 let code = r#"from utils use { foo };
1250
1251let x = foo();
1252"#;
1253 let program = parse_program(code).unwrap();
1254 let uri = Uri::from_file_path("/test.shape").unwrap();
1255 let cache = ModuleCache::new();
1256
1257 let location = find_imported_definition(&program, "foo", &uri, &cache);
1259 assert!(location.is_none());
1260 }
1261
1262 #[test]
1266 fn test_extract_base_type_name_plain() {
1267 assert_eq!(extract_base_type_name("Point"), Some("Point".to_string()));
1268 }
1269
1270 #[test]
1271 fn test_extract_base_type_name_array_wrapper() {
1272 assert_eq!(
1273 extract_base_type_name("Array<Point>"),
1274 Some("Point".to_string())
1275 );
1276 }
1277
1278 #[test]
1279 fn test_extract_base_type_name_option_question_mark() {
1280 assert_eq!(extract_base_type_name("Point?"), Some("Point".to_string()));
1281 }
1282
1283 #[test]
1284 fn test_extract_base_type_name_option_wrapper() {
1285 assert_eq!(
1286 extract_base_type_name("Option<Point>"),
1287 Some("Point".to_string())
1288 );
1289 }
1290
1291 #[test]
1292 fn test_extract_base_type_name_reference() {
1293 assert_eq!(
1294 extract_base_type_name("&mut Point"),
1295 Some("Point".to_string())
1296 );
1297 assert_eq!(extract_base_type_name("&Point"), Some("Point".to_string()));
1298 }
1299
1300 #[test]
1301 fn test_extract_base_type_name_nested() {
1302 assert_eq!(
1303 extract_base_type_name("Array<Option<Point>>"),
1304 Some("Point".to_string())
1305 );
1306 }
1307
1308 #[test]
1309 fn test_extract_base_type_name_result() {
1310 assert_eq!(
1311 extract_base_type_name("Result<Point, Error>"),
1312 Some("Point".to_string())
1313 );
1314 }
1315
1316 #[test]
1317 fn test_extract_base_type_name_object_shape_skipped() {
1318 assert_eq!(extract_base_type_name("{ x: int, y: int }"), None);
1320 }
1321
1322 #[test]
1323 fn test_is_builtin_primitive_filters_int() {
1324 assert!(is_builtin_primitive("int"));
1325 assert!(is_builtin_primitive("string"));
1326 assert!(!is_builtin_primitive("Point"));
1327 }
1328
1329 #[test]
1330 fn test_get_implementations_finds_impl_block() {
1331 let code = r#"trait Greet {
1332 method greet() -> string
1333}
1334
1335type Cat { name: string }
1336
1337impl Greet for Cat {
1338 method greet() { return "meow" }
1339}
1340"#;
1341 let program = parse_program(code).expect("test fixture must parse");
1343 let impl_count = program
1344 .items
1345 .iter()
1346 .filter(|i| matches!(i, Item::Impl(_, _)))
1347 .count();
1348 assert!(impl_count >= 1, "Expected at least 1 Item::Impl in parsed program");
1349
1350 let uri = Uri::from_file_path("/test.shape").unwrap();
1351 let impls = get_implementations(
1353 code,
1354 Position {
1355 line: 6,
1356 character: 6,
1357 },
1358 &uri,
1359 None,
1360 );
1361 assert!(impls.is_some(), "Should find impl block for trait Greet");
1362 let locations = impls.unwrap();
1363 assert_eq!(locations.len(), 1);
1364 }
1365
1366 #[test]
1367 fn test_get_implementations_by_target_type() {
1368 let code = r#"trait Greet {
1370 method greet() -> string
1371}
1372
1373type Cat { name: string }
1374
1375impl Greet for Cat {
1376 method greet() { return "meow" }
1377}
1378"#;
1379 let program = parse_program(code).expect("test fixture must parse");
1380 assert!(program.items.iter().any(|i| matches!(i, Item::Impl(_, _))));
1381
1382 let uri = Uri::from_file_path("/test.shape").unwrap();
1383 let impls = get_implementations(
1385 code,
1386 Position {
1387 line: 6,
1388 character: 16,
1389 },
1390 &uri,
1391 None,
1392 );
1393 assert!(
1394 impls.is_some(),
1395 "Should find impl block when cursor is on target type Cat"
1396 );
1397 }
1398
1399 #[test]
1400 fn test_get_declaration_aliases_definition() {
1401 let code = r#"let myVar = 42;
1402let x = myVar + 5;
1403"#;
1404 let uri = Uri::from_file_path("/test.shape").unwrap();
1405 let decl = get_declaration(
1407 code,
1408 Position {
1409 line: 1,
1410 character: 9,
1411 },
1412 &uri,
1413 None,
1414 None,
1415 None,
1416 );
1417 let def = get_definition(
1418 code,
1419 Position {
1420 line: 1,
1421 character: 9,
1422 },
1423 &uri,
1424 None,
1425 None,
1426 None,
1427 );
1428 assert_eq!(decl.is_some(), def.is_some());
1429 }
1430
1431 #[test]
1432 fn test_get_document_highlights_finds_variable_uses() {
1433 let code = r#"let myVar = 42;
1434let x = myVar + 5;
1435let y = myVar * 2;
1436"#;
1437 let highlights = get_document_highlights(
1439 code,
1440 Position {
1441 line: 0,
1442 character: 6,
1443 },
1444 None,
1445 );
1446 assert!(highlights.is_some(), "Should find highlights for myVar");
1447 let hs = highlights.unwrap();
1448 assert!(
1449 hs.len() >= 3,
1450 "Expected at least 3 highlights (def + 2 uses), got {}",
1451 hs.len()
1452 );
1453 for h in &hs {
1454 assert_eq!(h.kind, Some(DocumentHighlightKind::TEXT));
1455 }
1456 }
1457
1458 #[test]
1459 fn test_get_document_highlights_returns_none_off_symbol() {
1460 let code = "let myVar = 42;\n";
1461 let highlights = get_document_highlights(
1463 code,
1464 Position {
1465 line: 0,
1466 character: 0,
1467 },
1468 None,
1469 );
1470 let _ = highlights;
1473 }
1474
1475 #[test]
1476 fn test_is_module_scope_symbol_top_level_fn() {
1477 let code = "fn foo() { return 1 }\nlet x = foo()";
1478 let program = parse_program(code).unwrap();
1479 assert!(is_module_scope_symbol(&program, "foo"));
1480 assert!(is_module_scope_symbol(&program, "x"));
1481 assert!(!is_module_scope_symbol(&program, "nope"));
1483 }
1484
1485 #[test]
1486 fn test_collect_module_scope_refs_finds_call_site() {
1487 let text = "fn helper() { return 1 }\nlet x = helper() + helper()";
1488 let uri = Uri::from_file_path("/other.shape").unwrap();
1489 let mut out = Vec::new();
1490 collect_module_scope_refs_in_file(text, &uri, "helper", &mut out);
1491 assert!(
1493 out.len() >= 3,
1494 "expected def + at least 2 refs to `helper`, got {}",
1495 out.len()
1496 );
1497 }
1498
1499 #[test]
1500 fn test_get_references_cross_file_module_scope() {
1501 use crate::document::DocumentManager;
1503 let docs = DocumentManager::new();
1504 let main_text = "fn shared() { return 1 }\nlet a = shared()".to_string();
1505 let other_text = "fn shared() { return 2 }\nlet b = shared() + shared()".to_string();
1506 let main_uri = Uri::from_file_path("/main.shape").unwrap();
1507 let other_uri = Uri::from_file_path("/other.shape").unwrap();
1508 docs.open(main_uri.clone(), 1, main_text.clone());
1509 docs.open(other_uri.clone(), 1, other_text);
1510
1511 let pos = Position {
1513 line: 0,
1514 character: 3,
1515 };
1516 let refs = get_references_cross_file(
1517 &main_text,
1518 pos,
1519 &main_uri,
1520 None,
1521 Some(&docs),
1522 None,
1523 None,
1524 )
1525 .expect("should find cross-file references");
1526 assert!(
1528 refs.len() >= 4,
1529 "expected cross-file refs, got {}: {:?}",
1530 refs.len(),
1531 refs
1532 );
1533 assert!(
1534 refs.iter().any(|loc| &loc.uri == &other_uri),
1535 "expected at least one reference from /other.shape"
1536 );
1537 }
1538
1539 #[test]
1540 fn test_get_references_cross_file_local_binding_no_crossover() {
1541 use crate::document::DocumentManager;
1543 let docs = DocumentManager::new();
1544 let main_text =
1545 "fn outer() {\n let local = 1\n return local + local\n}".to_string();
1546 let other_text =
1547 "fn other() {\n let local = 5\n return local\n}".to_string();
1548 let main_uri = Uri::from_file_path("/main.shape").unwrap();
1549 let other_uri = Uri::from_file_path("/other.shape").unwrap();
1550 docs.open(main_uri.clone(), 1, main_text.clone());
1551 docs.open(other_uri.clone(), 1, other_text);
1552
1553 let local_offset = main_text.find("local").unwrap();
1555 let (line, col) = offset_to_line_col(&main_text, local_offset);
1556 let pos = Position {
1557 line,
1558 character: col,
1559 };
1560 let refs = get_references_cross_file(
1561 &main_text,
1562 pos,
1563 &main_uri,
1564 None,
1565 Some(&docs),
1566 None,
1567 None,
1568 );
1569 if let Some(refs) = refs {
1571 assert!(
1572 refs.iter().all(|loc| &loc.uri == &main_uri),
1573 "local-scope `local` should NOT cascade to other files, got: {:?}",
1574 refs
1575 );
1576 }
1577 }
1578
1579 #[test]
1580 fn test_references_with_broken_code() {
1581 let code =
1583 "fn greet(name) {\n return name\n}\nlet x = greet(\"hi\")\n??broken syntax here";
1584 let uri = Uri::from_file_path("/test.shape").unwrap();
1585
1586 let refs = get_references_with_fallback(
1588 code,
1589 Position {
1590 line: 0,
1591 character: 3,
1592 },
1593 &uri,
1594 None, );
1596
1597 assert!(
1599 refs.is_some(),
1600 "Should find references even with broken code via resilient parsing"
1601 );
1602 }
1603
1604 #[test]
1606 fn test_get_definition_returns_none_for_non_identifier_position() {
1607 let code = "let x = 5\n";
1608 let uri = Uri::from_file_path("/test.shape").unwrap();
1609 let result = get_definition(
1611 code,
1612 Position {
1613 line: 0,
1614 character: 6,
1615 },
1616 &uri,
1617 None,
1618 None,
1619 None,
1620 );
1621 assert!(result.is_none(), "expected None at non-identifier position");
1622 }
1623
1624 #[test]
1625 fn test_get_definition_finds_function_at_call_site() {
1626 let code = "fn myFunc() { return 1 }\nlet x = myFunc()\n";
1627 let uri = Uri::from_file_path("/test.shape").unwrap();
1628 let result = get_definition(
1630 code,
1631 Position {
1632 line: 1,
1633 character: 9,
1634 },
1635 &uri,
1636 None,
1637 None,
1638 None,
1639 );
1640 assert!(result.is_some(), "expected definition at call site");
1641 }
1642
1643 #[test]
1644 fn test_get_definition_unknown_symbol_returns_none() {
1645 let code = "let x = neverDefined\n";
1646 let uri = Uri::from_file_path("/test.shape").unwrap();
1647 let result = get_definition(
1649 code,
1650 Position {
1651 line: 0,
1652 character: 8,
1653 },
1654 &uri,
1655 None,
1656 None,
1657 None,
1658 );
1659 assert!(result.is_none(), "expected None for unknown symbol");
1660 }
1661
1662 #[test]
1663 fn test_get_type_definition_returns_none_for_unknown_symbol() {
1664 let code = "let x = 5\n";
1665 let uri = Uri::from_file_path("/test.shape").unwrap();
1666 let result = get_type_definition(
1667 code,
1668 Position {
1669 line: 0,
1670 character: 4,
1671 },
1672 &uri,
1673 None,
1674 None,
1675 );
1676 assert!(result.is_none(), "expected None for primitive type");
1678 }
1679
1680 #[test]
1681 fn test_get_implementations_returns_none_for_no_impls() {
1682 let code = "let x = 5\n";
1683 let uri = Uri::from_file_path("/test.shape").unwrap();
1684 let result = get_implementations(
1685 code,
1686 Position {
1687 line: 0,
1688 character: 4,
1689 },
1690 &uri,
1691 None,
1692 );
1693 assert!(result.is_none(), "expected None when no impls exist");
1694 }
1695
1696 #[test]
1697 fn test_get_implementations_finds_impl_by_trait_name() {
1698 let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1699 let uri = Uri::from_file_path("/test.shape").unwrap();
1700 let result = get_implementations(
1702 code,
1703 Position {
1704 line: 2,
1705 character: 5,
1706 },
1707 &uri,
1708 None,
1709 );
1710 assert!(result.is_some(), "expected impl-block location for Q");
1711 let locs = result.unwrap();
1712 assert!(!locs.is_empty());
1713 }
1714
1715 #[test]
1718 fn test_get_implementations_jumps_from_trait_method_to_impl_method() {
1719 let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1723 let uri = Uri::from_file_path("/test.shape").unwrap();
1724 let result = get_implementations(
1725 code,
1726 Position {
1727 line: 0,
1728 character: 13,
1729 },
1730 &uri,
1731 None,
1732 );
1733 assert!(
1734 result.is_some(),
1735 "expected impl-method location for trait-method 'q'"
1736 );
1737 let locs = result.unwrap();
1738 assert_eq!(locs.len(), 1, "expected exactly one impl-method match");
1739 assert_eq!(locs[0].range.start.line, 2);
1742 }
1743
1744 #[test]
1745 fn test_get_implementations_trait_method_multi_impls() {
1746 let code = "trait Greet {\n fn hello(self) -> string;\n}\ntype Cat { name: string }\ntype Dog { name: string }\nimpl Greet for Cat {\n fn hello(self) -> string { return \"meow\" }\n}\nimpl Greet for Dog {\n fn hello(self) -> string { return \"woof\" }\n}\n";
1749 let uri = Uri::from_file_path("/test.shape").unwrap();
1750 let result = get_implementations(
1752 code,
1753 Position {
1754 line: 1,
1755 character: 7,
1756 },
1757 &uri,
1758 None,
1759 );
1760 assert!(
1761 result.is_some(),
1762 "expected impl-method locations for trait-method 'hello'"
1763 );
1764 let locs = result.unwrap();
1765 assert_eq!(locs.len(), 2, "expected two impl-method matches");
1766 }
1767
1768 #[test]
1769 fn test_get_implementations_default_trait_method_still_finds_impls() {
1770 let code = "trait Greet {\n fn hello(self) -> string { return \"hi\" }\n}\ntype Cat { name: string }\nimpl Greet for Cat {\n fn hello(self) -> string { return \"meow\" }\n}\n";
1773 let uri = Uri::from_file_path("/test.shape").unwrap();
1774 let result = get_implementations(
1776 code,
1777 Position {
1778 line: 1,
1779 character: 7,
1780 },
1781 &uri,
1782 None,
1783 );
1784 assert!(
1785 result.is_some(),
1786 "expected impl-method location for default trait-method 'hello'"
1787 );
1788 let locs = result.unwrap();
1789 assert_eq!(locs.len(), 1);
1792 }
1793
1794 #[test]
1795 fn test_get_implementations_trait_name_still_returns_impl_block() {
1796 let code = "trait Q { fn q(self) -> int; }\ntype T { x: int }\nimpl Q for T { fn q(self) -> int { 1 } }\n";
1799 let uri = Uri::from_file_path("/test.shape").unwrap();
1800 let result = get_implementations(
1802 code,
1803 Position {
1804 line: 2,
1805 character: 5,
1806 },
1807 &uri,
1808 None,
1809 );
1810 assert!(result.is_some(), "trait-name path must still resolve");
1811 let locs = result.unwrap();
1812 assert_eq!(locs.len(), 1);
1813 assert_eq!(locs[0].range.start.line, 2);
1816 }
1817
1818 #[test]
1819 fn test_get_implementations_trait_method_no_matching_impl() {
1820 let code = "trait Q { fn q(self) -> int; }\n";
1824 let uri = Uri::from_file_path("/test.shape").unwrap();
1825 let result = get_implementations(
1826 code,
1827 Position {
1828 line: 0,
1829 character: 13,
1830 },
1831 &uri,
1832 None,
1833 );
1834 assert!(result.is_none());
1836 }
1837
1838 #[test]
1839 fn test_get_document_highlights_returns_none_off_word() {
1840 let code = "let x = 5\n";
1841 let result = get_document_highlights(
1842 code,
1843 Position {
1844 line: 0,
1845 character: 6, },
1847 None,
1848 );
1849 let _ = result;
1852 }
1853
1854 #[test]
1855 fn test_get_references_cross_file_no_documents_or_cache() {
1856 let code = "fn foo() { return 1 }\nlet x = foo()\n";
1857 let uri = Uri::from_file_path("/test.shape").unwrap();
1858 let result = get_references_cross_file(
1859 code,
1860 Position {
1861 line: 0,
1862 character: 3,
1863 },
1864 &uri,
1865 None, None, None, None, );
1870 assert!(result.is_some(), "expected local references when no cross-file context");
1872 }
1873
1874 #[test]
1875 fn test_get_declaration_aliases_get_definition() {
1876 let code = "fn foo() { return 1 }\nlet x = foo()\n";
1877 let uri = Uri::from_file_path("/test.shape").unwrap();
1878 let pos = Position {
1879 line: 1,
1880 character: 9,
1881 };
1882 let decl = get_declaration(code, pos, &uri, None, None, None);
1883 let def = get_definition(code, pos, &uri, None, None, None);
1884 assert_eq!(decl.is_some(), def.is_some());
1886 }
1887}