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