imports.rs

   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}