mirror of https://github.com/astral-sh/ruff
[ty] Add support for relative import completions
We already supported `from .. import <CURSOR>`, but we didn't support `from ..<CURSOR>`. This adds support for that.
This commit is contained in:
parent
553e568624
commit
1af318534a
|
|
@ -646,7 +646,16 @@ struct FromImport<'a> {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum FromImportKind {
|
enum FromImportKind {
|
||||||
Module,
|
Module,
|
||||||
Submodule { parent: ModuleName },
|
Submodule {
|
||||||
|
parent: ModuleName,
|
||||||
|
},
|
||||||
|
Relative {
|
||||||
|
parent: ModuleName,
|
||||||
|
/// When `true`, an `import` keyword is allowed next.
|
||||||
|
/// For example, `from ...<CURSOR>` should offer `import`
|
||||||
|
/// but also submodule completions.
|
||||||
|
import_keyword_allowed: bool,
|
||||||
|
},
|
||||||
Attribute,
|
Attribute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1048,20 +1057,51 @@ impl<'a> ImportStatement<'a> {
|
||||||
}
|
}
|
||||||
(Some(from), import) => {
|
(Some(from), import) => {
|
||||||
let ast = find_ast_for_from_import(parsed, from)?;
|
let ast = find_ast_for_from_import(parsed, from)?;
|
||||||
|
// If we saw an `import` keyword, then that means the
|
||||||
|
// cursor must be *after* the `import`. And thus we
|
||||||
|
// only ever need to offer completions for importable
|
||||||
|
// elements from the module being imported.
|
||||||
let kind = if import.is_some() {
|
let kind = if import.is_some() {
|
||||||
FromImportKind::Attribute
|
FromImportKind::Attribute
|
||||||
} else if initial_dot {
|
} else if !initial_dot {
|
||||||
// TODO: Handle relative imports here.
|
|
||||||
if to_complete.starts_with('.') {
|
|
||||||
return Some(ImportStatement::Incomplete(IncompleteImport::Import));
|
|
||||||
}
|
|
||||||
let (parent, _) = to_complete.rsplit_once('.')?;
|
|
||||||
let module_name = ModuleName::new(parent)?;
|
|
||||||
FromImportKind::Submodule {
|
|
||||||
parent: module_name,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FromImportKind::Module
|
FromImportKind::Module
|
||||||
|
} else {
|
||||||
|
let to_complete_without_leading_dots = to_complete.trim_start_matches('.');
|
||||||
|
|
||||||
|
// When there aren't any leading dots to trim, then we
|
||||||
|
// have a regular absolute import. Otherwise, it's relative.
|
||||||
|
if to_complete == to_complete_without_leading_dots {
|
||||||
|
let (parent, _) = to_complete.rsplit_once('.')?;
|
||||||
|
let parent = ModuleName::new(parent)?;
|
||||||
|
FromImportKind::Submodule { parent }
|
||||||
|
} else {
|
||||||
|
let all_dots = to_complete.chars().all(|c| c == '.');
|
||||||
|
// We should suggest `import` in `from ...<CURSOR>`
|
||||||
|
// and `from ...imp<CURSOR>`.
|
||||||
|
let import_keyword_allowed =
|
||||||
|
all_dots || !to_complete_without_leading_dots.contains('.');
|
||||||
|
let parent = if all_dots {
|
||||||
|
ModuleName::from_import_statement(db, file, ast).ok()?
|
||||||
|
} else {
|
||||||
|
// We know `to_complete` is not all dots.
|
||||||
|
// But that it starts with a dot.
|
||||||
|
// So we must have one of `..foo`, `..foo.`
|
||||||
|
// or `..foo.bar`. We drop the leading dots,
|
||||||
|
// since those are captured by `ast.level`.
|
||||||
|
// From there, we can treat it like a normal
|
||||||
|
// module name. We want to list submodule
|
||||||
|
// completions, so we pop off the last element
|
||||||
|
// if there are any remaining dots.
|
||||||
|
let parent = to_complete_without_leading_dots
|
||||||
|
.rsplit_once('.')
|
||||||
|
.map(|(parent, _)| parent);
|
||||||
|
ModuleName::from_identifier_parts(db, file, parent, ast.level).ok()?
|
||||||
|
};
|
||||||
|
FromImportKind::Relative {
|
||||||
|
parent,
|
||||||
|
import_keyword_allowed,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Some(ImportStatement::FromImport(FromImport { ast, kind }))
|
Some(ImportStatement::FromImport(FromImport { ast, kind }))
|
||||||
}
|
}
|
||||||
|
|
@ -1093,6 +1133,15 @@ impl<'a> ImportStatement<'a> {
|
||||||
FromImportKind::Submodule { ref parent } => {
|
FromImportKind::Submodule { ref parent } => {
|
||||||
completions.extend(model.import_submodule_completions_for_name(parent));
|
completions.extend(model.import_submodule_completions_for_name(parent));
|
||||||
}
|
}
|
||||||
|
FromImportKind::Relative {
|
||||||
|
ref parent,
|
||||||
|
import_keyword_allowed,
|
||||||
|
} => {
|
||||||
|
completions.extend(model.import_submodule_completions_for_name(parent));
|
||||||
|
if import_keyword_allowed {
|
||||||
|
completions.try_add(Completion::keyword("import"));
|
||||||
|
}
|
||||||
|
}
|
||||||
FromImportKind::Attribute => {
|
FromImportKind::Attribute => {
|
||||||
completions.extend(model.from_import_completions(ast));
|
completions.extend(model.from_import_completions(ast));
|
||||||
}
|
}
|
||||||
|
|
@ -5178,9 +5227,9 @@ match status:
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_import_importt_suggests_import() {
|
fn from_import_importt_suggests_nothing() {
|
||||||
let builder = completion_test_builder("from typing importt<CURSOR>");
|
let builder = completion_test_builder("from typing importt<CURSOR>");
|
||||||
assert_snapshot!(builder.build().snapshot(), @"import");
|
assert_snapshot!(builder.build().snapshot(), @"<No completions found>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -5207,12 +5256,10 @@ match status:
|
||||||
assert_snapshot!(builder.build().snapshot(), @"import");
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The following behaviour may not be reflected in editors, since LSP
|
|
||||||
/// clients may do their own filtering of completion suggestions.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_import_random_name_suggests_import() {
|
fn from_import_random_name_suggests_nothing() {
|
||||||
let builder = completion_test_builder("from typing aa<CURSOR>");
|
let builder = completion_test_builder("from typing aa<CURSOR>");
|
||||||
assert_snapshot!(builder.build().snapshot(), @"import");
|
assert_snapshot!(builder.build().snapshot(), @"<No completions found>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -5261,29 +5308,37 @@ match status:
|
||||||
#[test]
|
#[test]
|
||||||
fn from_import_only_dot() {
|
fn from_import_only_dot() {
|
||||||
let builder = CursorTest::builder()
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
.source(
|
.source(
|
||||||
"main.py",
|
"package/sub1/sub2/bar.py",
|
||||||
"
|
"
|
||||||
import_zqzqzq = 1
|
import_zqzqzq = 1
|
||||||
from .<CURSOR>
|
from .<CURSOR>
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.completion_test_builder();
|
.completion_test_builder();
|
||||||
assert_snapshot!(builder.build().snapshot(), @"import");
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_import_only_dot_incomplete() {
|
fn from_import_only_dot_incomplete() {
|
||||||
let builder = CursorTest::builder()
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
.source(
|
.source(
|
||||||
"main.py",
|
"package/sub1/sub2/bar.py",
|
||||||
"
|
"
|
||||||
import_zqzqzq = 1
|
import_zqzqzq = 1
|
||||||
from .imp<CURSOR>
|
from .imp<CURSOR>
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.completion_test_builder();
|
.completion_test_builder();
|
||||||
assert_snapshot!(builder.build().snapshot(), @"import");
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -5297,6 +5352,114 @@ match status:
|
||||||
assert_snapshot!(builder.build().snapshot(), @"ZQZQZQ");
|
assert_snapshot!(builder.build().snapshot(), @"ZQZQZQ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_dots1() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from ...<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
foo
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_dots2() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo/__init__.py", "")
|
||||||
|
.source("package/foo/bar.py", "")
|
||||||
|
.source("package/foo/baz.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from ...foo.<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
bar
|
||||||
|
baz
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_dots3() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from.<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_dots4() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
|
.source("package/sub1/bar.py", "from ..<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
foo
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_typing1() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from ...fo<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @"foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_typing2() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo/__init__.py", "")
|
||||||
|
.source("package/foo/bar.py", "")
|
||||||
|
.source("package/foo/baz.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from ...foo.ba<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
bar
|
||||||
|
baz
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_typing3() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/foo.py", "")
|
||||||
|
.source("package/imposition.py", "")
|
||||||
|
.source("package/sub1/sub2/bar.py", "from ...im<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
import
|
||||||
|
imposition
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_import_module_after_typing4() {
|
||||||
|
let builder = CursorTest::builder()
|
||||||
|
.source("package/__init__.py", "")
|
||||||
|
.source("package/sub1/__init__.py", "")
|
||||||
|
.source("package/sub1/foo.py", "")
|
||||||
|
.source("package/sub1/imposition.py", "")
|
||||||
|
.source("package/sub1/bar.py", "from ..sub1.<CURSOR>")
|
||||||
|
.completion_test_builder();
|
||||||
|
assert_snapshot!(builder.build().snapshot(), @r"
|
||||||
|
bar
|
||||||
|
foo
|
||||||
|
imposition
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
/// A way to create a simple single-file (named `main.py`) completion test
|
/// A way to create a simple single-file (named `main.py`) completion test
|
||||||
/// builder.
|
/// builder.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ impl ModuleName {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the absolute module name from the LHS components of `from LHS import RHS`
|
/// Computes the absolute module name from the LHS components of `from LHS import RHS`
|
||||||
pub(crate) fn from_identifier_parts(
|
pub fn from_identifier_parts(
|
||||||
db: &dyn Db,
|
db: &dyn Db,
|
||||||
importing_file: File,
|
importing_file: File,
|
||||||
module: Option<&str>,
|
module: Option<&str>,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue