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    const 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    const 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}