1use collections::HashMap;
2use language::BufferSnapshot;
3use language::ImportsConfig;
4use language::Language;
5use std::ops::Deref;
6use std::path::Path;
7use std::sync::Arc;
8use std::{borrow::Cow, ops::Range};
9use text::OffsetRangeExt as _;
10use util::RangeExt;
11use util::paths::PathStyle;
12
13use crate::OccurrenceSource;
14use crate::{Identifier, IdentifierParts, Occurrences};
15
16// TODO: Write documentation for extension authors. The @import capture must match before or in the
17// same pattern as all all captures it contains
18
19// Future improvements to consider:
20//
21// * Distinguish absolute vs relative paths in captures. `#include "maths.h"` is relative whereas
22// `#include <maths.h>` is not.
23//
24// * Provide the name used when importing whole modules (see tests with "named_module" in the name).
25// To be useful, will require parsing of identifier qualification.
26//
27// * Scoping for imports that aren't at the top level
28//
29// * Only scan a prefix of the file, when possible. This could look like having query matches that
30// indicate it reached a declaration that is not allowed in the import section.
31//
32// * Support directly parsing to occurrences instead of storing namespaces / paths. Types should be
33// generic on this, so that tests etc can still use strings. Could do similar in syntax index.
34//
35// * Distinguish different types of namespaces when known. E.g. "name.type" capture. Once capture
36// names are more open-ended like this may make sense to build and cache a jump table (direct
37// dispatch from capture index).
38//
39// * There are a few "Language specific:" comments on behavior that gets applied to all languages.
40// Would be cleaner to be conditional on the language or otherwise configured.
41
42#[derive(Debug, Clone, Default)]
43pub struct Imports {
44 pub identifier_to_imports: HashMap<Identifier, Vec<Import>>,
45 pub wildcard_modules: Vec<Module>,
46}
47
48#[derive(Debug, Clone)]
49pub enum Import {
50 Direct {
51 module: Module,
52 },
53 Alias {
54 module: Module,
55 external_identifier: Identifier,
56 },
57}
58
59#[derive(Debug, Clone)]
60pub enum Module {
61 SourceExact(Arc<Path>),
62 SourceFuzzy(Arc<Path>),
63 Namespace(Namespace),
64}
65
66impl Module {
67 fn empty() -> Self {
68 Module::Namespace(Namespace::default())
69 }
70
71 fn push_range(
72 &mut self,
73 range: &ModuleRange,
74 snapshot: &BufferSnapshot,
75 language: &Language,
76 parent_abs_path: Option<&Path>,
77 ) -> usize {
78 if range.is_empty() {
79 return 0;
80 }
81
82 match range {
83 ModuleRange::Source(range) => {
84 if let Self::Namespace(namespace) = self
85 && namespace.0.is_empty()
86 {
87 let path = snapshot.text_for_range(range.clone()).collect::<Cow<str>>();
88
89 let path = if let Some(strip_regex) =
90 language.config().import_path_strip_regex.as_ref()
91 {
92 strip_regex.replace_all(&path, "")
93 } else {
94 path
95 };
96
97 let path = Path::new(path.as_ref());
98 if (path.starts_with(".") || path.starts_with(".."))
99 && let Some(parent_abs_path) = parent_abs_path
100 && let Ok(abs_path) =
101 util::paths::normalize_lexically(&parent_abs_path.join(path))
102 {
103 *self = Self::SourceExact(abs_path.into());
104 } else {
105 *self = Self::SourceFuzzy(path.into());
106 };
107 } else if matches!(self, Self::SourceExact(_))
108 || matches!(self, Self::SourceFuzzy(_))
109 {
110 log::warn!("bug in imports query: encountered multiple @source matches");
111 } else {
112 log::warn!(
113 "bug in imports query: encountered both @namespace and @source match"
114 );
115 }
116 }
117 ModuleRange::Namespace(range) => {
118 if let Self::Namespace(namespace) = self {
119 let segment = range_text(snapshot, range);
120 if language.config().ignored_import_segments.contains(&segment) {
121 return 0;
122 } else {
123 namespace.0.push(segment);
124 return 1;
125 }
126 } else {
127 log::warn!(
128 "bug in imports query: encountered both @namespace and @source match"
129 );
130 }
131 }
132 }
133 0
134 }
135}
136
137#[derive(Debug, Clone)]
138enum ModuleRange {
139 Source(Range<usize>),
140 Namespace(Range<usize>),
141}
142
143impl Deref for ModuleRange {
144 type Target = Range<usize>;
145
146 fn deref(&self) -> &Self::Target {
147 match self {
148 ModuleRange::Source(range) => range,
149 ModuleRange::Namespace(range) => range,
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Default)]
155pub struct Namespace(pub Vec<Arc<str>>);
156
157impl Namespace {
158 pub fn occurrences(&self) -> Occurrences<IdentifierParts> {
159 Occurrences::new(
160 self.0
161 .iter()
162 .flat_map(|identifier| IdentifierParts::occurrences_in_str(&identifier)),
163 )
164 }
165}
166
167impl Imports {
168 pub fn gather(snapshot: &BufferSnapshot, parent_abs_path: Option<&Path>) -> Self {
169 // Query to match different import patterns
170 let mut matches = snapshot
171 .syntax
172 .matches(0..snapshot.len(), &snapshot.text, |grammar| {
173 grammar.imports_config().map(|imports| &imports.query)
174 });
175
176 let mut detached_nodes: Vec<DetachedNode> = Vec::new();
177 let mut identifier_to_imports = HashMap::default();
178 let mut wildcard_modules = Vec::new();
179 let mut import_range = None;
180
181 while let Some(query_match) = matches.peek() {
182 let ImportsConfig {
183 query: _,
184 import_ix,
185 name_ix,
186 namespace_ix,
187 source_ix,
188 list_ix,
189 wildcard_ix,
190 alias_ix,
191 } = matches.grammars()[query_match.grammar_index]
192 .imports_config()
193 .unwrap();
194
195 let mut new_import_range = None;
196 let mut alias_range = None;
197 let mut modules = Vec::new();
198 let mut content: Option<(Range<usize>, ContentKind)> = None;
199 for capture in query_match.captures {
200 let capture_range = capture.node.byte_range();
201
202 if capture.index == *import_ix {
203 new_import_range = Some(capture_range);
204 } else if Some(capture.index) == *namespace_ix {
205 modules.push(ModuleRange::Namespace(capture_range));
206 } else if Some(capture.index) == *source_ix {
207 modules.push(ModuleRange::Source(capture_range));
208 } else if Some(capture.index) == *alias_ix {
209 alias_range = Some(capture_range);
210 } else {
211 let mut found_content = None;
212 if Some(capture.index) == *name_ix {
213 found_content = Some((capture_range, ContentKind::Name));
214 } else if Some(capture.index) == *list_ix {
215 found_content = Some((capture_range, ContentKind::List));
216 } else if Some(capture.index) == *wildcard_ix {
217 found_content = Some((capture_range, ContentKind::Wildcard));
218 }
219 if let Some((found_content_range, found_kind)) = found_content {
220 if let Some((_, old_kind)) = content {
221 let point = found_content_range.to_point(snapshot);
222 log::warn!(
223 "bug in {} imports query: unexpected multiple captures of {} and {} ({}:{}:{})",
224 query_match.language.name(),
225 old_kind.capture_name(),
226 found_kind.capture_name(),
227 snapshot
228 .file()
229 .map(|p| p.path().display(PathStyle::Posix))
230 .unwrap_or_default(),
231 point.start.row + 1,
232 point.start.column + 1
233 );
234 }
235 content = Some((found_content_range, found_kind));
236 }
237 }
238 }
239
240 if let Some(new_import_range) = new_import_range {
241 log::trace!("starting new import {:?}", new_import_range);
242 Self::gather_from_import_statement(
243 &detached_nodes,
244 &snapshot,
245 parent_abs_path,
246 &mut identifier_to_imports,
247 &mut wildcard_modules,
248 );
249 detached_nodes.clear();
250 import_range = Some(new_import_range.clone());
251 }
252
253 if let Some((content, content_kind)) = content {
254 if import_range
255 .as_ref()
256 .is_some_and(|import_range| import_range.contains_inclusive(&content))
257 {
258 detached_nodes.push(DetachedNode {
259 modules,
260 content: content.clone(),
261 content_kind,
262 alias: alias_range.unwrap_or(0..0),
263 language: query_match.language.clone(),
264 });
265 } else {
266 log::trace!(
267 "filtered out match not inside import range: {content_kind:?} at {content:?}"
268 );
269 }
270 }
271
272 matches.advance();
273 }
274
275 Self::gather_from_import_statement(
276 &detached_nodes,
277 &snapshot,
278 parent_abs_path,
279 &mut identifier_to_imports,
280 &mut wildcard_modules,
281 );
282
283 Imports {
284 identifier_to_imports,
285 wildcard_modules,
286 }
287 }
288
289 fn gather_from_import_statement(
290 detached_nodes: &[DetachedNode],
291 snapshot: &BufferSnapshot,
292 parent_abs_path: Option<&Path>,
293 identifier_to_imports: &mut HashMap<Identifier, Vec<Import>>,
294 wildcard_modules: &mut Vec<Module>,
295 ) {
296 let mut trees = Vec::new();
297
298 for detached_node in detached_nodes {
299 if let Some(node) = Self::attach_node(detached_node.into(), &mut trees) {
300 trees.push(node);
301 }
302 log::trace!(
303 "Attached node to tree\n{:#?}\nAttach result:\n{:#?}",
304 detached_node,
305 trees
306 .iter()
307 .map(|tree| tree.debug(snapshot))
308 .collect::<Vec<_>>()
309 );
310 }
311
312 for tree in &trees {
313 let mut module = Module::empty();
314 Self::gather_from_tree(
315 tree,
316 snapshot,
317 parent_abs_path,
318 &mut module,
319 identifier_to_imports,
320 wildcard_modules,
321 );
322 }
323 }
324
325 fn attach_node(mut node: ImportTree, trees: &mut Vec<ImportTree>) -> Option<ImportTree> {
326 let mut tree_index = 0;
327 while tree_index < trees.len() {
328 let tree = &mut trees[tree_index];
329 if !node.content.is_empty() && node.content == tree.content {
330 // multiple matches can apply to the same name/list/wildcard. This keeps the queries
331 // simpler by combining info from these matches.
332 if tree.module.is_empty() {
333 tree.module = node.module;
334 tree.module_children = node.module_children;
335 }
336 if tree.alias.is_empty() {
337 tree.alias = node.alias;
338 }
339 return None;
340 } else if !node.module.is_empty() && node.module.contains_inclusive(&tree.range()) {
341 node.module_children.push(trees.remove(tree_index));
342 continue;
343 } else if !node.content.is_empty() && node.content.contains_inclusive(&tree.content) {
344 node.content_children.push(trees.remove(tree_index));
345 continue;
346 } else if !tree.content.is_empty() && tree.content.contains_inclusive(&node.content) {
347 if let Some(node) = Self::attach_node(node, &mut tree.content_children) {
348 tree.content_children.push(node);
349 }
350 return None;
351 }
352 tree_index += 1;
353 }
354 Some(node)
355 }
356
357 fn gather_from_tree(
358 tree: &ImportTree,
359 snapshot: &BufferSnapshot,
360 parent_abs_path: Option<&Path>,
361 current_module: &mut Module,
362 identifier_to_imports: &mut HashMap<Identifier, Vec<Import>>,
363 wildcard_modules: &mut Vec<Module>,
364 ) {
365 let mut pop_count = 0;
366
367 if tree.module_children.is_empty() {
368 pop_count +=
369 current_module.push_range(&tree.module, snapshot, &tree.language, parent_abs_path);
370 } else {
371 for child in &tree.module_children {
372 pop_count += Self::extend_namespace_from_tree(
373 child,
374 snapshot,
375 parent_abs_path,
376 current_module,
377 );
378 }
379 };
380
381 if tree.content_children.is_empty() && !tree.content.is_empty() {
382 match tree.content_kind {
383 ContentKind::Name | ContentKind::List => {
384 if tree.alias.is_empty() {
385 identifier_to_imports
386 .entry(Identifier {
387 language_id: tree.language.id(),
388 name: range_text(snapshot, &tree.content),
389 })
390 .or_default()
391 .push(Import::Direct {
392 module: current_module.clone(),
393 });
394 } else {
395 let alias_name: Arc<str> = range_text(snapshot, &tree.alias);
396 let external_name = range_text(snapshot, &tree.content);
397 // Language specific: skip "_" aliases for Rust
398 if alias_name.as_ref() != "_" {
399 identifier_to_imports
400 .entry(Identifier {
401 language_id: tree.language.id(),
402 name: alias_name,
403 })
404 .or_default()
405 .push(Import::Alias {
406 module: current_module.clone(),
407 external_identifier: Identifier {
408 language_id: tree.language.id(),
409 name: external_name,
410 },
411 });
412 }
413 }
414 }
415 ContentKind::Wildcard => wildcard_modules.push(current_module.clone()),
416 }
417 } else {
418 for child in &tree.content_children {
419 Self::gather_from_tree(
420 child,
421 snapshot,
422 parent_abs_path,
423 current_module,
424 identifier_to_imports,
425 wildcard_modules,
426 );
427 }
428 }
429
430 if pop_count > 0 {
431 match current_module {
432 Module::SourceExact(_) | Module::SourceFuzzy(_) => {
433 log::warn!(
434 "bug in imports query: encountered both @namespace and @source match"
435 );
436 }
437 Module::Namespace(namespace) => {
438 namespace.0.drain(namespace.0.len() - pop_count..);
439 }
440 }
441 }
442 }
443
444 fn extend_namespace_from_tree(
445 tree: &ImportTree,
446 snapshot: &BufferSnapshot,
447 parent_abs_path: Option<&Path>,
448 module: &mut Module,
449 ) -> usize {
450 let mut pop_count = 0;
451 if tree.module_children.is_empty() {
452 pop_count += module.push_range(&tree.module, snapshot, &tree.language, parent_abs_path);
453 } else {
454 for child in &tree.module_children {
455 pop_count +=
456 Self::extend_namespace_from_tree(child, snapshot, parent_abs_path, module);
457 }
458 }
459 if tree.content_children.is_empty() {
460 pop_count += module.push_range(
461 &ModuleRange::Namespace(tree.content.clone()),
462 snapshot,
463 &tree.language,
464 parent_abs_path,
465 );
466 } else {
467 for child in &tree.content_children {
468 pop_count +=
469 Self::extend_namespace_from_tree(child, snapshot, parent_abs_path, module);
470 }
471 }
472 pop_count
473 }
474}
475
476fn range_text(snapshot: &BufferSnapshot, range: &Range<usize>) -> Arc<str> {
477 snapshot
478 .text_for_range(range.clone())
479 .collect::<Cow<str>>()
480 .into()
481}
482
483#[derive(Debug)]
484struct DetachedNode {
485 modules: Vec<ModuleRange>,
486 content: Range<usize>,
487 content_kind: ContentKind,
488 alias: Range<usize>,
489 language: Arc<Language>,
490}
491
492#[derive(Debug, Clone, Copy)]
493enum ContentKind {
494 Name,
495 Wildcard,
496 List,
497}
498
499impl ContentKind {
500 fn capture_name(&self) -> &'static str {
501 match self {
502 ContentKind::Name => "name",
503 ContentKind::Wildcard => "wildcard",
504 ContentKind::List => "list",
505 }
506 }
507}
508
509#[derive(Debug)]
510struct ImportTree {
511 module: ModuleRange,
512 /// When non-empty, provides namespace / source info which should be used instead of `module`.
513 module_children: Vec<ImportTree>,
514 content: Range<usize>,
515 /// When non-empty, provides content which should be used instead of `content`.
516 content_children: Vec<ImportTree>,
517 content_kind: ContentKind,
518 alias: Range<usize>,
519 language: Arc<Language>,
520}
521
522impl ImportTree {
523 fn range(&self) -> Range<usize> {
524 self.module.start.min(self.content.start)..self.module.end.max(self.content.end)
525 }
526
527 #[allow(dead_code)]
528 fn debug<'a>(&'a self, snapshot: &'a BufferSnapshot) -> ImportTreeDebug<'a> {
529 ImportTreeDebug {
530 tree: self,
531 snapshot,
532 }
533 }
534
535 fn from_module_range(module: &ModuleRange, language: Arc<Language>) -> Self {
536 ImportTree {
537 module: module.clone(),
538 module_children: Vec::new(),
539 content: 0..0,
540 content_children: Vec::new(),
541 content_kind: ContentKind::Name,
542 alias: 0..0,
543 language,
544 }
545 }
546}
547
548impl From<&DetachedNode> for ImportTree {
549 fn from(value: &DetachedNode) -> Self {
550 let module;
551 let module_children;
552 match value.modules.len() {
553 0 => {
554 module = ModuleRange::Namespace(0..0);
555 module_children = Vec::new();
556 }
557 1 => {
558 module = value.modules[0].clone();
559 module_children = Vec::new();
560 }
561 _ => {
562 module = ModuleRange::Namespace(
563 value.modules.first().unwrap().start..value.modules.last().unwrap().end,
564 );
565 module_children = value
566 .modules
567 .iter()
568 .map(|module| ImportTree::from_module_range(module, value.language.clone()))
569 .collect();
570 }
571 }
572
573 ImportTree {
574 module,
575 module_children,
576 content: value.content.clone(),
577 content_children: Vec::new(),
578 content_kind: value.content_kind,
579 alias: value.alias.clone(),
580 language: value.language.clone(),
581 }
582 }
583}
584
585struct ImportTreeDebug<'a> {
586 tree: &'a ImportTree,
587 snapshot: &'a BufferSnapshot,
588}
589
590impl std::fmt::Debug for ImportTreeDebug<'_> {
591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592 f.debug_struct("ImportTree")
593 .field("module_range", &self.tree.module)
594 .field("module_text", &range_text(self.snapshot, &self.tree.module))
595 .field(
596 "module_children",
597 &self
598 .tree
599 .module_children
600 .iter()
601 .map(|child| child.debug(&self.snapshot))
602 .collect::<Vec<Self>>(),
603 )
604 .field("content_range", &self.tree.content)
605 .field(
606 "content_text",
607 &range_text(self.snapshot, &self.tree.content),
608 )
609 .field(
610 "content_children",
611 &self
612 .tree
613 .content_children
614 .iter()
615 .map(|child| child.debug(&self.snapshot))
616 .collect::<Vec<Self>>(),
617 )
618 .field("content_kind", &self.tree.content_kind)
619 .field("alias_range", &self.tree.alias)
620 .field("alias_text", &range_text(self.snapshot, &self.tree.alias))
621 .finish()
622 }
623}
624
625#[cfg(test)]
626mod test {
627 use std::path::PathBuf;
628 use std::sync::{Arc, LazyLock};
629
630 use super::*;
631 use collections::HashSet;
632 use gpui::{TestAppContext, prelude::*};
633 use indoc::indoc;
634 use language::{
635 Buffer, Language, LanguageConfig, tree_sitter_python, tree_sitter_rust,
636 tree_sitter_typescript,
637 };
638 use regex::Regex;
639
640 #[gpui::test]
641 fn test_rust_simple(cx: &mut TestAppContext) {
642 check_imports(
643 &RUST,
644 "use std::collections::HashMap;",
645 &[&["std", "collections", "HashMap"]],
646 cx,
647 );
648
649 check_imports(
650 &RUST,
651 "pub use std::collections::HashMap;",
652 &[&["std", "collections", "HashMap"]],
653 cx,
654 );
655
656 check_imports(
657 &RUST,
658 "use std::collections::{HashMap, HashSet};",
659 &[
660 &["std", "collections", "HashMap"],
661 &["std", "collections", "HashSet"],
662 ],
663 cx,
664 );
665 }
666
667 #[gpui::test]
668 fn test_rust_nested(cx: &mut TestAppContext) {
669 check_imports(
670 &RUST,
671 "use std::{any::TypeId, collections::{HashMap, HashSet}};",
672 &[
673 &["std", "any", "TypeId"],
674 &["std", "collections", "HashMap"],
675 &["std", "collections", "HashSet"],
676 ],
677 cx,
678 );
679
680 check_imports(
681 &RUST,
682 "use a::b::c::{d::e::F, g::h::I};",
683 &[
684 &["a", "b", "c", "d", "e", "F"],
685 &["a", "b", "c", "g", "h", "I"],
686 ],
687 cx,
688 );
689 }
690
691 #[gpui::test]
692 fn test_rust_multiple_imports(cx: &mut TestAppContext) {
693 check_imports(
694 &RUST,
695 indoc! {"
696 use std::collections::HashMap;
697 use std::any::{TypeId, Any};
698 "},
699 &[
700 &["std", "collections", "HashMap"],
701 &["std", "any", "TypeId"],
702 &["std", "any", "Any"],
703 ],
704 cx,
705 );
706
707 check_imports(
708 &RUST,
709 indoc! {"
710 use std::collections::HashSet;
711
712 fn main() {
713 let unqualified = HashSet::new();
714 let qualified = std::collections::HashMap::new();
715 }
716
717 use std::any::TypeId;
718 "},
719 &[
720 &["std", "collections", "HashSet"],
721 &["std", "any", "TypeId"],
722 ],
723 cx,
724 );
725 }
726
727 #[gpui::test]
728 fn test_rust_wildcard(cx: &mut TestAppContext) {
729 check_imports(&RUST, "use prelude::*;", &[&["prelude", "WILDCARD"]], cx);
730
731 check_imports(
732 &RUST,
733 "use zed::prelude::*;",
734 &[&["zed", "prelude", "WILDCARD"]],
735 cx,
736 );
737
738 check_imports(&RUST, "use prelude::{*};", &[&["prelude", "WILDCARD"]], cx);
739
740 check_imports(
741 &RUST,
742 "use prelude::{File, *};",
743 &[&["prelude", "File"], &["prelude", "WILDCARD"]],
744 cx,
745 );
746
747 check_imports(
748 &RUST,
749 "use zed::{App, prelude::*};",
750 &[&["zed", "App"], &["zed", "prelude", "WILDCARD"]],
751 cx,
752 );
753 }
754
755 #[gpui::test]
756 fn test_rust_alias(cx: &mut TestAppContext) {
757 check_imports(
758 &RUST,
759 "use std::io::Result as IoResult;",
760 &[&["std", "io", "Result AS IoResult"]],
761 cx,
762 );
763 }
764
765 #[gpui::test]
766 fn test_rust_crate_and_super(cx: &mut TestAppContext) {
767 check_imports(&RUST, "use crate::a::b::c;", &[&["a", "b", "c"]], cx);
768 check_imports(&RUST, "use super::a::b::c;", &[&["a", "b", "c"]], cx);
769 // TODO: Consider stripping leading "::". Not done for now because for the text similarity matching usecase this
770 // is fine.
771 check_imports(&RUST, "use ::a::b::c;", &[&["::a", "b", "c"]], cx);
772 }
773
774 #[gpui::test]
775 fn test_typescript_imports(cx: &mut TestAppContext) {
776 let parent_abs_path = PathBuf::from("/home/user/project");
777
778 check_imports_with_file_abs_path(
779 Some(&parent_abs_path),
780 &TYPESCRIPT,
781 r#"import "./maths.js";"#,
782 &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
783 cx,
784 );
785
786 check_imports_with_file_abs_path(
787 Some(&parent_abs_path),
788 &TYPESCRIPT,
789 r#"import "../maths.js";"#,
790 &[&["SOURCE /home/user/maths", "WILDCARD"]],
791 cx,
792 );
793
794 check_imports_with_file_abs_path(
795 Some(&parent_abs_path),
796 &TYPESCRIPT,
797 r#"import RandomNumberGenerator, { pi as π } from "./maths.js";"#,
798 &[
799 &["SOURCE /home/user/project/maths", "RandomNumberGenerator"],
800 &["SOURCE /home/user/project/maths", "pi AS π"],
801 ],
802 cx,
803 );
804
805 check_imports_with_file_abs_path(
806 Some(&parent_abs_path),
807 &TYPESCRIPT,
808 r#"import { pi, phi, absolute } from "./maths.js";"#,
809 &[
810 &["SOURCE /home/user/project/maths", "pi"],
811 &["SOURCE /home/user/project/maths", "phi"],
812 &["SOURCE /home/user/project/maths", "absolute"],
813 ],
814 cx,
815 );
816
817 // index.js is removed by import_path_strip_regex
818 check_imports_with_file_abs_path(
819 Some(&parent_abs_path),
820 &TYPESCRIPT,
821 r#"import { pi, phi, absolute } from "./maths/index.js";"#,
822 &[
823 &["SOURCE /home/user/project/maths", "pi"],
824 &["SOURCE /home/user/project/maths", "phi"],
825 &["SOURCE /home/user/project/maths", "absolute"],
826 ],
827 cx,
828 );
829
830 check_imports_with_file_abs_path(
831 Some(&parent_abs_path),
832 &TYPESCRIPT,
833 r#"import type { SomeThing } from "./some-module.js";"#,
834 &[&["SOURCE /home/user/project/some-module", "SomeThing"]],
835 cx,
836 );
837
838 check_imports_with_file_abs_path(
839 Some(&parent_abs_path),
840 &TYPESCRIPT,
841 r#"import { type SomeThing, OtherThing } from "./some-module.js";"#,
842 &[
843 &["SOURCE /home/user/project/some-module", "SomeThing"],
844 &["SOURCE /home/user/project/some-module", "OtherThing"],
845 ],
846 cx,
847 );
848
849 // index.js is removed by import_path_strip_regex
850 check_imports_with_file_abs_path(
851 Some(&parent_abs_path),
852 &TYPESCRIPT,
853 r#"import { type SomeThing, OtherThing } from "./some-module/index.js";"#,
854 &[
855 &["SOURCE /home/user/project/some-module", "SomeThing"],
856 &["SOURCE /home/user/project/some-module", "OtherThing"],
857 ],
858 cx,
859 );
860
861 // fuzzy paths
862 check_imports_with_file_abs_path(
863 Some(&parent_abs_path),
864 &TYPESCRIPT,
865 r#"import { type SomeThing, OtherThing } from "@my-app/some-module.js";"#,
866 &[
867 &["SOURCE FUZZY @my-app/some-module", "SomeThing"],
868 &["SOURCE FUZZY @my-app/some-module", "OtherThing"],
869 ],
870 cx,
871 );
872 }
873
874 #[gpui::test]
875 fn test_typescript_named_module_imports(cx: &mut TestAppContext) {
876 let parent_abs_path = PathBuf::from("/home/user/project");
877
878 // TODO: These should provide the name that the module is bound to.
879 // For now instead these are treated as unqualified wildcard imports.
880 check_imports_with_file_abs_path(
881 Some(&parent_abs_path),
882 &TYPESCRIPT,
883 r#"import * as math from "./maths.js";"#,
884 // &[&["/home/user/project/maths.js", "WILDCARD AS math"]],
885 &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
886 cx,
887 );
888 check_imports_with_file_abs_path(
889 Some(&parent_abs_path),
890 &TYPESCRIPT,
891 r#"import math = require("./maths");"#,
892 // &[&["/home/user/project/maths", "WILDCARD AS math"]],
893 &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
894 cx,
895 );
896 }
897
898 #[gpui::test]
899 fn test_python_imports(cx: &mut TestAppContext) {
900 check_imports(&PYTHON, "from math import pi", &[&["math", "pi"]], cx);
901
902 check_imports(
903 &PYTHON,
904 "from math import pi, sin, cos",
905 &[&["math", "pi"], &["math", "sin"], &["math", "cos"]],
906 cx,
907 );
908
909 check_imports(&PYTHON, "from math import *", &[&["math", "WILDCARD"]], cx);
910
911 check_imports(
912 &PYTHON,
913 "from math import foo.bar.baz",
914 &[&["math", "foo", "bar", "baz"]],
915 cx,
916 );
917
918 check_imports(
919 &PYTHON,
920 "from math import pi as PI",
921 &[&["math", "pi AS PI"]],
922 cx,
923 );
924
925 check_imports(
926 &PYTHON,
927 "from serializers.json import JsonSerializer",
928 &[&["serializers", "json", "JsonSerializer"]],
929 cx,
930 );
931
932 check_imports(
933 &PYTHON,
934 "from custom.serializers import json, xml, yaml",
935 &[
936 &["custom", "serializers", "json"],
937 &["custom", "serializers", "xml"],
938 &["custom", "serializers", "yaml"],
939 ],
940 cx,
941 );
942 }
943
944 #[gpui::test]
945 fn test_python_named_module_imports(cx: &mut TestAppContext) {
946 // TODO: These should provide the name that the module is bound to.
947 // For now instead these are treated as unqualified wildcard imports.
948 //
949 // check_imports(&PYTHON, "import math", &[&["math", "WILDCARD as math"]], cx);
950 // check_imports(&PYTHON, "import math as maths", &[&["math", "WILDCARD AS maths"]], cx);
951 //
952 // Something like:
953 //
954 // (import_statement
955 // name: [
956 // (dotted_name
957 // (identifier)* @namespace
958 // (identifier) @name.module .)
959 // (aliased_import
960 // name: (dotted_name
961 // ((identifier) ".")* @namespace
962 // (identifier) @name.module .)
963 // alias: (identifier) @alias)
964 // ]) @import
965
966 check_imports(&PYTHON, "import math", &[&["math", "WILDCARD"]], cx);
967
968 check_imports(
969 &PYTHON,
970 "import math as maths",
971 &[&["math", "WILDCARD"]],
972 cx,
973 );
974
975 check_imports(&PYTHON, "import a.b.c", &[&["a", "b", "c", "WILDCARD"]], cx);
976
977 check_imports(
978 &PYTHON,
979 "import a.b.c as d",
980 &[&["a", "b", "c", "WILDCARD"]],
981 cx,
982 );
983 }
984
985 #[gpui::test]
986 fn test_python_package_relative_imports(cx: &mut TestAppContext) {
987 // TODO: These should provide info about the dir they are relative to, to provide more
988 // precise resolution. Instead, fuzzy matching is used as usual.
989
990 check_imports(&PYTHON, "from . import math", &[&["math"]], cx);
991
992 check_imports(&PYTHON, "from .a import math", &[&["a", "math"]], cx);
993
994 check_imports(
995 &PYTHON,
996 "from ..a.b import math",
997 &[&["a", "b", "math"]],
998 cx,
999 );
1000
1001 check_imports(
1002 &PYTHON,
1003 "from ..a.b import *",
1004 &[&["a", "b", "WILDCARD"]],
1005 cx,
1006 );
1007 }
1008
1009 #[gpui::test]
1010 fn test_c_imports(cx: &mut TestAppContext) {
1011 let parent_abs_path = PathBuf::from("/home/user/project");
1012
1013 // TODO: Distinguish that these are not relative to current path
1014 check_imports_with_file_abs_path(
1015 Some(&parent_abs_path),
1016 &C,
1017 r#"#include <math.h>"#,
1018 &[&["SOURCE FUZZY math.h", "WILDCARD"]],
1019 cx,
1020 );
1021
1022 // TODO: These should be treated as relative, but don't start with ./ or ../
1023 check_imports_with_file_abs_path(
1024 Some(&parent_abs_path),
1025 &C,
1026 r#"#include "math.h""#,
1027 &[&["SOURCE FUZZY math.h", "WILDCARD"]],
1028 cx,
1029 );
1030 }
1031
1032 #[gpui::test]
1033 fn test_cpp_imports(cx: &mut TestAppContext) {
1034 let parent_abs_path = PathBuf::from("/home/user/project");
1035
1036 // TODO: Distinguish that these are not relative to current path
1037 check_imports_with_file_abs_path(
1038 Some(&parent_abs_path),
1039 &CPP,
1040 r#"#include <math.h>"#,
1041 &[&["SOURCE FUZZY math.h", "WILDCARD"]],
1042 cx,
1043 );
1044
1045 // TODO: These should be treated as relative, but don't start with ./ or ../
1046 check_imports_with_file_abs_path(
1047 Some(&parent_abs_path),
1048 &CPP,
1049 r#"#include "math.h""#,
1050 &[&["SOURCE FUZZY math.h", "WILDCARD"]],
1051 cx,
1052 );
1053 }
1054
1055 #[gpui::test]
1056 fn test_go_imports(cx: &mut TestAppContext) {
1057 check_imports(
1058 &GO,
1059 r#"import . "lib/math""#,
1060 &[&["lib/math", "WILDCARD"]],
1061 cx,
1062 );
1063
1064 // not included, these are only for side-effects
1065 check_imports(&GO, r#"import _ "lib/math""#, &[], cx);
1066 }
1067
1068 #[gpui::test]
1069 fn test_go_named_module_imports(cx: &mut TestAppContext) {
1070 // TODO: These should provide the name that the module is bound to.
1071 // For now instead these are treated as unqualified wildcard imports.
1072
1073 check_imports(
1074 &GO,
1075 r#"import "lib/math""#,
1076 &[&["lib/math", "WILDCARD"]],
1077 cx,
1078 );
1079 check_imports(
1080 &GO,
1081 r#"import m "lib/math""#,
1082 &[&["lib/math", "WILDCARD"]],
1083 cx,
1084 );
1085 }
1086
1087 #[track_caller]
1088 fn check_imports(
1089 language: &Arc<Language>,
1090 source: &str,
1091 expected: &[&[&str]],
1092 cx: &mut TestAppContext,
1093 ) {
1094 check_imports_with_file_abs_path(None, language, source, expected, cx);
1095 }
1096
1097 #[track_caller]
1098 fn check_imports_with_file_abs_path(
1099 parent_abs_path: Option<&Path>,
1100 language: &Arc<Language>,
1101 source: &str,
1102 expected: &[&[&str]],
1103 cx: &mut TestAppContext,
1104 ) {
1105 let buffer = cx.new(|cx| {
1106 let mut buffer = Buffer::local(source, cx);
1107 buffer.set_language(Some(language.clone()), cx);
1108 buffer
1109 });
1110 cx.run_until_parked();
1111
1112 let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1113
1114 let imports = Imports::gather(&snapshot, parent_abs_path);
1115 let mut actual_symbols = imports
1116 .identifier_to_imports
1117 .iter()
1118 .flat_map(|(identifier, imports)| {
1119 imports
1120 .iter()
1121 .map(|import| import.to_identifier_parts(identifier.name.as_ref()))
1122 })
1123 .chain(
1124 imports
1125 .wildcard_modules
1126 .iter()
1127 .map(|module| module.to_identifier_parts("WILDCARD")),
1128 )
1129 .collect::<Vec<_>>();
1130 let mut expected_symbols = expected
1131 .iter()
1132 .map(|expected| expected.iter().map(|s| s.to_string()).collect::<Vec<_>>())
1133 .collect::<Vec<_>>();
1134 actual_symbols.sort();
1135 expected_symbols.sort();
1136 if actual_symbols != expected_symbols {
1137 let top_layer = snapshot.syntax_layers().next().unwrap();
1138 panic!(
1139 "Expected imports: {:?}\n\
1140 Actual imports: {:?}\n\
1141 Tree:\n{}",
1142 expected_symbols,
1143 actual_symbols,
1144 tree_to_string(&top_layer.node()),
1145 );
1146 }
1147 }
1148
1149 fn tree_to_string(node: &tree_sitter::Node) -> String {
1150 let mut cursor = node.walk();
1151 let mut result = String::new();
1152 let mut depth = 0;
1153 'outer: loop {
1154 result.push_str(&" ".repeat(depth));
1155 if let Some(field_name) = cursor.field_name() {
1156 result.push_str(field_name);
1157 result.push_str(": ");
1158 }
1159 if cursor.node().is_named() {
1160 result.push_str(cursor.node().kind());
1161 } else {
1162 result.push('"');
1163 result.push_str(cursor.node().kind());
1164 result.push('"');
1165 }
1166 result.push('\n');
1167
1168 if cursor.goto_first_child() {
1169 depth += 1;
1170 continue;
1171 }
1172 if cursor.goto_next_sibling() {
1173 continue;
1174 }
1175 while cursor.goto_parent() {
1176 depth -= 1;
1177 if cursor.goto_next_sibling() {
1178 continue 'outer;
1179 }
1180 }
1181 break;
1182 }
1183 result
1184 }
1185
1186 static RUST: LazyLock<Arc<Language>> = LazyLock::new(|| {
1187 Arc::new(
1188 Language::new(
1189 LanguageConfig {
1190 name: "Rust".into(),
1191 ignored_import_segments: HashSet::from_iter(["crate".into(), "super".into()]),
1192 import_path_strip_regex: Some(Regex::new("/(lib|mod)\\.rs$").unwrap()),
1193 ..Default::default()
1194 },
1195 Some(tree_sitter_rust::LANGUAGE.into()),
1196 )
1197 .with_imports_query(include_str!("../../languages/src/rust/imports.scm"))
1198 .unwrap(),
1199 )
1200 });
1201
1202 static TYPESCRIPT: LazyLock<Arc<Language>> = LazyLock::new(|| {
1203 Arc::new(
1204 Language::new(
1205 LanguageConfig {
1206 name: "TypeScript".into(),
1207 import_path_strip_regex: Some(Regex::new("(?:/index)?\\.[jt]s$").unwrap()),
1208 ..Default::default()
1209 },
1210 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
1211 )
1212 .with_imports_query(include_str!("../../languages/src/typescript/imports.scm"))
1213 .unwrap(),
1214 )
1215 });
1216
1217 static PYTHON: LazyLock<Arc<Language>> = LazyLock::new(|| {
1218 Arc::new(
1219 Language::new(
1220 LanguageConfig {
1221 name: "Python".into(),
1222 import_path_strip_regex: Some(Regex::new("/__init__\\.py$").unwrap()),
1223 ..Default::default()
1224 },
1225 Some(tree_sitter_python::LANGUAGE.into()),
1226 )
1227 .with_imports_query(include_str!("../../languages/src/python/imports.scm"))
1228 .unwrap(),
1229 )
1230 });
1231
1232 // TODO: Ideally should use actual language configurations
1233 static C: LazyLock<Arc<Language>> = LazyLock::new(|| {
1234 Arc::new(
1235 Language::new(
1236 LanguageConfig {
1237 name: "C".into(),
1238 import_path_strip_regex: Some(Regex::new("^<|>$").unwrap()),
1239 ..Default::default()
1240 },
1241 Some(tree_sitter_c::LANGUAGE.into()),
1242 )
1243 .with_imports_query(include_str!("../../languages/src/c/imports.scm"))
1244 .unwrap(),
1245 )
1246 });
1247
1248 static CPP: LazyLock<Arc<Language>> = LazyLock::new(|| {
1249 Arc::new(
1250 Language::new(
1251 LanguageConfig {
1252 name: "C++".into(),
1253 import_path_strip_regex: Some(Regex::new("^<|>$").unwrap()),
1254 ..Default::default()
1255 },
1256 Some(tree_sitter_cpp::LANGUAGE.into()),
1257 )
1258 .with_imports_query(include_str!("../../languages/src/cpp/imports.scm"))
1259 .unwrap(),
1260 )
1261 });
1262
1263 static GO: LazyLock<Arc<Language>> = LazyLock::new(|| {
1264 Arc::new(
1265 Language::new(
1266 LanguageConfig {
1267 name: "Go".into(),
1268 ..Default::default()
1269 },
1270 Some(tree_sitter_go::LANGUAGE.into()),
1271 )
1272 .with_imports_query(include_str!("../../languages/src/go/imports.scm"))
1273 .unwrap(),
1274 )
1275 });
1276
1277 impl Import {
1278 fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
1279 match self {
1280 Import::Direct { module } => module.to_identifier_parts(identifier),
1281 Import::Alias {
1282 module,
1283 external_identifier: external_name,
1284 } => {
1285 module.to_identifier_parts(&format!("{} AS {}", external_name.name, identifier))
1286 }
1287 }
1288 }
1289 }
1290
1291 impl Module {
1292 fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
1293 match self {
1294 Self::Namespace(namespace) => namespace.to_identifier_parts(identifier),
1295 Self::SourceExact(path) => {
1296 vec![
1297 format!("SOURCE {}", path.display().to_string().replace("\\", "/")),
1298 identifier.to_string(),
1299 ]
1300 }
1301 Self::SourceFuzzy(path) => {
1302 vec![
1303 format!(
1304 "SOURCE FUZZY {}",
1305 path.display().to_string().replace("\\", "/")
1306 ),
1307 identifier.to_string(),
1308 ]
1309 }
1310 }
1311 }
1312 }
1313
1314 impl Namespace {
1315 fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
1316 self.0
1317 .iter()
1318 .map(|chunk| chunk.to_string())
1319 .chain(std::iter::once(identifier.to_string()))
1320 .collect::<Vec<_>>()
1321 }
1322 }
1323}