mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Wire up "list modules" API to make module completions work
This makes `import <CURSOR>` and `from <CURSOR>` completions work. This also makes `import os.<CURSOR>` and `from os.<CURSOR>` completions work. In this case, we are careful to only offer submodule completions.
This commit is contained in:
parent
05478d5cc7
commit
2e9c241d7e
2 changed files with 396 additions and 16 deletions
|
@ -23,7 +23,18 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'
|
||||||
let model = SemanticModel::new(db, file);
|
let model = SemanticModel::new(db, file);
|
||||||
let mut completions = match target {
|
let mut completions = match target {
|
||||||
CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr),
|
CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr),
|
||||||
CompletionTargetAst::ImportFrom { import, name } => model.import_completions(import, name),
|
CompletionTargetAst::ObjectDotInImport { import, name } => {
|
||||||
|
model.import_submodule_completions(import, name)
|
||||||
|
}
|
||||||
|
CompletionTargetAst::ObjectDotInImportFrom { import } => {
|
||||||
|
model.from_import_submodule_completions(import)
|
||||||
|
}
|
||||||
|
CompletionTargetAst::ImportFrom { import, name } => {
|
||||||
|
model.from_import_completions(import, name)
|
||||||
|
}
|
||||||
|
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
||||||
|
model.import_completions()
|
||||||
|
}
|
||||||
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
|
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
|
||||||
};
|
};
|
||||||
completions.sort_by(compare_suggestions);
|
completions.sort_by(compare_suggestions);
|
||||||
|
@ -50,11 +61,11 @@ enum CompletionTargetTokens<'t> {
|
||||||
object: &'t Token,
|
object: &'t Token,
|
||||||
/// The token, if non-empty, following the dot.
|
/// The token, if non-empty, following the dot.
|
||||||
///
|
///
|
||||||
/// This is currently unused, but we should use this
|
/// For right now, this is only used to determine which
|
||||||
/// eventually to remove completions that aren't a
|
/// module in an `import` statement to return submodule
|
||||||
/// prefix of what has already been typed. (We are
|
/// completions for. But we could use it for other things,
|
||||||
/// currently relying on the LSP client to do this.)
|
/// like only returning completions that start with a prefix
|
||||||
#[expect(dead_code)]
|
/// corresponding to this token.
|
||||||
attribute: Option<&'t Token>,
|
attribute: Option<&'t Token>,
|
||||||
},
|
},
|
||||||
/// A `from module import attribute` token form was found, where
|
/// A `from module import attribute` token form was found, where
|
||||||
|
@ -63,6 +74,20 @@ enum CompletionTargetTokens<'t> {
|
||||||
/// The module being imported from.
|
/// The module being imported from.
|
||||||
module: &'t Token,
|
module: &'t Token,
|
||||||
},
|
},
|
||||||
|
/// A `import module` token form was found, where `module` may be
|
||||||
|
/// empty.
|
||||||
|
Import {
|
||||||
|
/// The token corresponding to the `import` keyword.
|
||||||
|
import: &'t Token,
|
||||||
|
/// The token closest to the cursor.
|
||||||
|
///
|
||||||
|
/// This is currently unused, but we should use this
|
||||||
|
/// eventually to remove completions that aren't a
|
||||||
|
/// prefix of what has already been typed. (We are
|
||||||
|
/// currently relying on the LSP client to do this.)
|
||||||
|
#[expect(dead_code)]
|
||||||
|
module: &'t Token,
|
||||||
|
},
|
||||||
/// A token was found under the cursor, but it didn't
|
/// A token was found under the cursor, but it didn't
|
||||||
/// match any of our anticipated token patterns.
|
/// match any of our anticipated token patterns.
|
||||||
Generic { token: &'t Token },
|
Generic { token: &'t Token },
|
||||||
|
@ -105,6 +130,8 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||||
}
|
}
|
||||||
} else if let Some(module) = import_from_tokens(before) {
|
} else if let Some(module) = import_from_tokens(before) {
|
||||||
CompletionTargetTokens::ImportFrom { module }
|
CompletionTargetTokens::ImportFrom { module }
|
||||||
|
} else if let Some((import, module)) = import_tokens(before) {
|
||||||
|
CompletionTargetTokens::Import { import, module }
|
||||||
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) {
|
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) {
|
||||||
// If we're writing a `float`, then we should
|
// If we're writing a `float`, then we should
|
||||||
// specifically not offer completions. This wouldn't
|
// specifically not offer completions. This wouldn't
|
||||||
|
@ -140,19 +167,47 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||||
offset: TextSize,
|
offset: TextSize,
|
||||||
) -> Option<CompletionTargetAst<'t>> {
|
) -> Option<CompletionTargetAst<'t>> {
|
||||||
match *self {
|
match *self {
|
||||||
CompletionTargetTokens::PossibleObjectDot { object, .. } => {
|
CompletionTargetTokens::PossibleObjectDot { object, attribute } => {
|
||||||
let covering_node = covering_node(parsed.syntax().into(), object.range())
|
let covering_node = covering_node(parsed.syntax().into(), object.range())
|
||||||
// We require that the end of the node range not
|
.find_last(|node| {
|
||||||
// exceed the cursor offset. This avoids selecting
|
// We require that the end of the node range not
|
||||||
// a node "too high" in the AST in cases where
|
// exceed the cursor offset. This avoids selecting
|
||||||
// completions are requested in the middle of an
|
// a node "too high" in the AST in cases where
|
||||||
// expression. e.g., `foo.<CURSOR>.bar`.
|
// completions are requested in the middle of an
|
||||||
.find_last(|node| node.is_expr_attribute() && node.range().end() <= offset)
|
// expression. e.g., `foo.<CURSOR>.bar`.
|
||||||
|
if node.is_expr_attribute() {
|
||||||
|
return node.range().end() <= offset;
|
||||||
|
}
|
||||||
|
// For import statements though, they can't be
|
||||||
|
// nested, so we don't care as much about the
|
||||||
|
// cursor being strictly after the statement.
|
||||||
|
// And indeed, sometimes it won't be! e.g.,
|
||||||
|
//
|
||||||
|
// import re, os.p<CURSOR>, zlib
|
||||||
|
//
|
||||||
|
// So just return once we find an import.
|
||||||
|
node.is_stmt_import() || node.is_stmt_import_from()
|
||||||
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
match covering_node.node() {
|
match covering_node.node() {
|
||||||
ast::AnyNodeRef::ExprAttribute(expr) => {
|
ast::AnyNodeRef::ExprAttribute(expr) => {
|
||||||
Some(CompletionTargetAst::ObjectDot { expr })
|
Some(CompletionTargetAst::ObjectDot { expr })
|
||||||
}
|
}
|
||||||
|
ast::AnyNodeRef::StmtImport(import) => {
|
||||||
|
let range = attribute
|
||||||
|
.map(Ranged::range)
|
||||||
|
.unwrap_or_else(|| object.range());
|
||||||
|
// Find the name that overlaps with the
|
||||||
|
// token we identified for the attribute.
|
||||||
|
let name = import
|
||||||
|
.names
|
||||||
|
.iter()
|
||||||
|
.position(|alias| alias.range().contains_range(range))?;
|
||||||
|
Some(CompletionTargetAst::ObjectDotInImport { import, name })
|
||||||
|
}
|
||||||
|
ast::AnyNodeRef::StmtImportFrom(import) => {
|
||||||
|
Some(CompletionTargetAst::ObjectDotInImportFrom { import })
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,6 +220,20 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||||
};
|
};
|
||||||
Some(CompletionTargetAst::ImportFrom { import, name: None })
|
Some(CompletionTargetAst::ImportFrom { import, name: None })
|
||||||
}
|
}
|
||||||
|
CompletionTargetTokens::Import { import, .. } => {
|
||||||
|
let covering_node = covering_node(parsed.syntax().into(), import.range())
|
||||||
|
.find_first(|node| node.is_stmt_import() || node.is_stmt_import_from())
|
||||||
|
.ok()?;
|
||||||
|
match covering_node.node() {
|
||||||
|
ast::AnyNodeRef::StmtImport(import) => {
|
||||||
|
Some(CompletionTargetAst::Import { import, name: None })
|
||||||
|
}
|
||||||
|
ast::AnyNodeRef::StmtImportFrom(import) => {
|
||||||
|
Some(CompletionTargetAst::ImportViaFrom { import })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
CompletionTargetTokens::Generic { token } => {
|
CompletionTargetTokens::Generic { token } => {
|
||||||
let covering_node = covering_node(parsed.syntax().into(), token.range());
|
let covering_node = covering_node(parsed.syntax().into(), token.range());
|
||||||
Some(CompletionTargetAst::Scoped {
|
Some(CompletionTargetAst::Scoped {
|
||||||
|
@ -188,6 +257,18 @@ enum CompletionTargetAst<'t> {
|
||||||
/// A `object.attribute` scenario, where we want to
|
/// A `object.attribute` scenario, where we want to
|
||||||
/// list attributes on `object` for completions.
|
/// list attributes on `object` for completions.
|
||||||
ObjectDot { expr: &'t ast::ExprAttribute },
|
ObjectDot { expr: &'t ast::ExprAttribute },
|
||||||
|
/// A `import module.submodule` scenario, where we only want to
|
||||||
|
/// list submodules for completions.
|
||||||
|
ObjectDotInImport {
|
||||||
|
/// The import statement.
|
||||||
|
import: &'t ast::StmtImport,
|
||||||
|
/// An index into `import.names`. The index is guaranteed to be
|
||||||
|
/// valid.
|
||||||
|
name: usize,
|
||||||
|
},
|
||||||
|
/// A `from module.submodule` scenario, where we only want to list
|
||||||
|
/// submodules for completions.
|
||||||
|
ObjectDotInImportFrom { import: &'t ast::StmtImportFrom },
|
||||||
/// A `from module import attribute` scenario, where we want to
|
/// A `from module import attribute` scenario, where we want to
|
||||||
/// list attributes on `module` for completions.
|
/// list attributes on `module` for completions.
|
||||||
ImportFrom {
|
ImportFrom {
|
||||||
|
@ -197,6 +278,24 @@ enum CompletionTargetAst<'t> {
|
||||||
/// set, the index is guaranteed to be valid.
|
/// set, the index is guaranteed to be valid.
|
||||||
name: Option<usize>,
|
name: Option<usize>,
|
||||||
},
|
},
|
||||||
|
/// A `import module` scenario, where we want to
|
||||||
|
/// list available modules for completions.
|
||||||
|
Import {
|
||||||
|
/// The import statement.
|
||||||
|
#[expect(dead_code)]
|
||||||
|
import: &'t ast::StmtImport,
|
||||||
|
/// An index into `import.names` if relevant. When this is
|
||||||
|
/// set, the index is guaranteed to be valid.
|
||||||
|
#[expect(dead_code)]
|
||||||
|
name: Option<usize>,
|
||||||
|
},
|
||||||
|
/// A `from module` scenario, where we want to
|
||||||
|
/// list available modules for completions.
|
||||||
|
ImportViaFrom {
|
||||||
|
/// The import statement.
|
||||||
|
#[expect(dead_code)]
|
||||||
|
import: &'t ast::StmtImportFrom,
|
||||||
|
},
|
||||||
/// A scoped scenario, where we want to list all items available in
|
/// A scoped scenario, where we want to list all items available in
|
||||||
/// the most narrow scope containing the giving AST node.
|
/// the most narrow scope containing the giving AST node.
|
||||||
Scoped { node: ast::AnyNodeRef<'t> },
|
Scoped { node: ast::AnyNodeRef<'t> },
|
||||||
|
@ -317,6 +416,52 @@ fn import_from_tokens(tokens: &[Token]) -> Option<&Token> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks for the start of a `import <CURSOR>` statement.
|
||||||
|
///
|
||||||
|
/// This also handles cases like `import foo, c<CURSOR>, bar`.
|
||||||
|
///
|
||||||
|
/// If found, a token corresponding to the `import` or `from` keyword
|
||||||
|
/// and the the closest point of the `<CURSOR>` is returned.
|
||||||
|
///
|
||||||
|
/// It is assumed that callers will call `from_import_tokens` first to
|
||||||
|
/// try and recognize a `from ... import ...` statement before using
|
||||||
|
/// this.
|
||||||
|
fn import_tokens(tokens: &[Token]) -> Option<(&Token, &Token)> {
|
||||||
|
use TokenKind as TK;
|
||||||
|
|
||||||
|
/// A look-back limit, in order to bound work.
|
||||||
|
///
|
||||||
|
/// See `LIMIT` in `import_from_tokens` for more context.
|
||||||
|
const LIMIT: usize = 1_000;
|
||||||
|
|
||||||
|
/// A state used to "parse" the tokens preceding the user's cursor,
|
||||||
|
/// in reverse, to detect a `import` statement.
|
||||||
|
enum S {
|
||||||
|
Start,
|
||||||
|
Names,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = S::Start;
|
||||||
|
let module_token = tokens.last()?;
|
||||||
|
// Move backward through the tokens until we get to
|
||||||
|
// the `import` token.
|
||||||
|
for token in tokens.iter().rev().take(LIMIT) {
|
||||||
|
state = match (state, token.kind()) {
|
||||||
|
// It's okay to pop off a newline token here initially,
|
||||||
|
// since it may occur when the name being imported is
|
||||||
|
// empty.
|
||||||
|
(S::Start, TK::Newline) => S::Names,
|
||||||
|
// Munch through tokens that can make up an alias.
|
||||||
|
(S::Start | S::Names, TK::Name | TK::Comma | TK::As | TK::Unknown) => S::Names,
|
||||||
|
(S::Start | S::Names, TK::Import | TK::From) => {
|
||||||
|
return Some((token, module_token));
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Order completions lexicographically, with these exceptions:
|
/// Order completions lexicographically, with these exceptions:
|
||||||
///
|
///
|
||||||
/// 1) A `_[^_]` prefix sorts last and
|
/// 1) A `_[^_]` prefix sorts last and
|
||||||
|
@ -2709,6 +2854,143 @@ importlib.<CURSOR>
|
||||||
test.assert_completions_include("resources");
|
test.assert_completions_include("resources");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_with_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import c<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_without_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import <CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_multiple() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import re, c<CURSOR>, sys
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_with_aliases() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import re as regexp, c<CURSOR>, sys as system
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_over_multiple_lines() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import re as regexp, \\
|
||||||
|
c<CURSOR>, \\
|
||||||
|
sys as system
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_unknown_in_module() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import ?, <CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_via_from_with_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
from c<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_via_from_without_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
from <CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_statement_with_submodule_with_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import os.p<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("path");
|
||||||
|
test.assert_completions_do_not_include("abspath");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_statement_with_submodule_multiple() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import re, os.p<CURSOR>, zlib
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("path");
|
||||||
|
test.assert_completions_do_not_include("abspath");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_statement_with_submodule_without_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
import os.<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("path");
|
||||||
|
test.assert_completions_do_not_include("abspath");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_via_from_with_submodule_with_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
from os.p<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("path");
|
||||||
|
test.assert_completions_do_not_include("abspath");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_via_from_with_submodule_without_leading_character() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"\
|
||||||
|
from os.<CURSOR>
|
||||||
|
",
|
||||||
|
);
|
||||||
|
test.assert_completions_include("path");
|
||||||
|
test.assert_completions_do_not_include("abspath");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regression_test_issue_642() {
|
fn regression_test_issue_642() {
|
||||||
// Regression test for https://github.com/astral-sh/ty/issues/642
|
// Regression test for https://github.com/astral-sh/ty/issues/642
|
||||||
|
|
|
@ -6,7 +6,7 @@ use ruff_source_file::LineIndex;
|
||||||
|
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
use crate::module_name::ModuleName;
|
use crate::module_name::ModuleName;
|
||||||
use crate::module_resolver::{KnownModule, Module, resolve_module};
|
use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
|
||||||
use crate::semantic_index::definition::Definition;
|
use crate::semantic_index::definition::Definition;
|
||||||
use crate::semantic_index::scope::FileScopeId;
|
use crate::semantic_index::scope::FileScopeId;
|
||||||
use crate::semantic_index::semantic_index;
|
use crate::semantic_index::semantic_index;
|
||||||
|
@ -41,8 +41,24 @@ impl<'db> SemanticModel<'db> {
|
||||||
resolve_module(self.db, module_name)
|
resolve_module(self.db, module_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns completions for symbols available in a `import <CURSOR>` context.
|
||||||
|
pub fn import_completions(&self) -> Vec<Completion<'db>> {
|
||||||
|
list_modules(self.db)
|
||||||
|
.into_iter()
|
||||||
|
.map(|module| {
|
||||||
|
let builtin = module.is_known(self.db, KnownModule::Builtins);
|
||||||
|
let ty = Type::module_literal(self.db, self.file, module);
|
||||||
|
Completion {
|
||||||
|
name: Name::new(module.name(self.db).as_str()),
|
||||||
|
ty,
|
||||||
|
builtin,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns completions for symbols available in a `from module import <CURSOR>` context.
|
/// Returns completions for symbols available in a `from module import <CURSOR>` context.
|
||||||
pub fn import_completions(
|
pub fn from_import_completions(
|
||||||
&self,
|
&self,
|
||||||
import: &ast::StmtImportFrom,
|
import: &ast::StmtImportFrom,
|
||||||
_name: Option<usize>,
|
_name: Option<usize>,
|
||||||
|
@ -61,6 +77,79 @@ impl<'db> SemanticModel<'db> {
|
||||||
self.module_completions(&module_name)
|
self.module_completions(&module_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns completions only for submodules for the module
|
||||||
|
/// identified by `name` in `import`.
|
||||||
|
///
|
||||||
|
/// For example, `import re, os.<CURSOR>, zlib`.
|
||||||
|
pub fn import_submodule_completions(
|
||||||
|
&self,
|
||||||
|
import: &ast::StmtImport,
|
||||||
|
name: usize,
|
||||||
|
) -> Vec<Completion<'db>> {
|
||||||
|
let module_ident = &import.names[name].name;
|
||||||
|
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let module_name =
|
||||||
|
match ModuleName::from_identifier_parts(self.db, self.file, Some(parent_ident), 0) {
|
||||||
|
Ok(module_name) => module_name,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Could not extract module name from `{module:?}`: {err:?}",
|
||||||
|
module = module_ident,
|
||||||
|
);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.import_submodule_completions_for_name(&module_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns completions only for submodules for the module
|
||||||
|
/// used in a `from module import attribute` statement.
|
||||||
|
///
|
||||||
|
/// For example, `from os.<CURSOR>`.
|
||||||
|
pub fn from_import_submodule_completions(
|
||||||
|
&self,
|
||||||
|
import: &ast::StmtImportFrom,
|
||||||
|
) -> Vec<Completion<'db>> {
|
||||||
|
let level = import.level;
|
||||||
|
let Some(module_ident) = import.module.as_deref() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let module_name = match ModuleName::from_identifier_parts(
|
||||||
|
self.db,
|
||||||
|
self.file,
|
||||||
|
Some(parent_ident),
|
||||||
|
level,
|
||||||
|
) {
|
||||||
|
Ok(module_name) => module_name,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Could not extract module name from `{module:?}` with level {level}: {err:?}",
|
||||||
|
module = import.module,
|
||||||
|
level = import.level,
|
||||||
|
);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.import_submodule_completions_for_name(&module_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns submodule-only completions for the given module.
|
||||||
|
fn import_submodule_completions_for_name(
|
||||||
|
&self,
|
||||||
|
module_name: &ModuleName,
|
||||||
|
) -> Vec<Completion<'db>> {
|
||||||
|
let Some(module) = resolve_module(self.db, module_name) else {
|
||||||
|
tracing::debug!("Could not resolve module from `{module_name:?}`");
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
self.submodule_completions(&module)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns completions for symbols available in the given module as if
|
/// Returns completions for symbols available in the given module as if
|
||||||
/// it were imported by this model's `File`.
|
/// it were imported by this model's `File`.
|
||||||
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion<'db>> {
|
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion<'db>> {
|
||||||
|
@ -75,11 +164,20 @@ impl<'db> SemanticModel<'db> {
|
||||||
for crate::types::Member { name, ty } in crate::types::all_members(self.db, ty) {
|
for crate::types::Member { name, ty } in crate::types::all_members(self.db, ty) {
|
||||||
completions.push(Completion { name, ty, builtin });
|
completions.push(Completion { name, ty, builtin });
|
||||||
}
|
}
|
||||||
|
completions.extend(self.submodule_completions(&module));
|
||||||
|
completions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns completions for submodules of the given module.
|
||||||
|
fn submodule_completions(&self, module: &Module<'db>) -> Vec<Completion<'db>> {
|
||||||
|
let builtin = module.is_known(self.db, KnownModule::Builtins);
|
||||||
|
|
||||||
|
let mut completions = vec![];
|
||||||
for submodule_basename in module.all_submodules(self.db) {
|
for submodule_basename in module.all_submodules(self.db) {
|
||||||
let Some(basename) = ModuleName::new(submodule_basename.as_str()) else {
|
let Some(basename) = ModuleName::new(submodule_basename.as_str()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut submodule_name = module_name.clone();
|
let mut submodule_name = module.name(self.db).clone();
|
||||||
submodule_name.extend(&basename);
|
submodule_name.extend(&basename);
|
||||||
|
|
||||||
let Some(submodule) = resolve_module(self.db, &submodule_name) else {
|
let Some(submodule) = resolve_module(self.db, &submodule_name) else {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue