mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-17 22:07:42 +00:00
[ty] Add some completion ranking improvements (#20807)
Co-authored-by: Micha Reiser <micha@reiser.io> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
4fc7dd300c
commit
651f7963a7
20 changed files with 753 additions and 93 deletions
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
|
@ -721,7 +721,7 @@ jobs:
|
|||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run ty completion evaluation"
|
||||
run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
|
||||
run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
|
||||
- name: "Ensure there are no changes"
|
||||
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ To run a full evaluation, run the `ty_completion_eval` crate with the
|
|||
`all` command from the root of this repository:
|
||||
|
||||
```console
|
||||
cargo run --release --package ty_completion_eval -- all
|
||||
cargo run --profile profiling --package ty_completion_eval -- all
|
||||
```
|
||||
|
||||
The output should look like this:
|
||||
|
@ -24,7 +24,7 @@ you can ask the evaluation to write CSV data that contains the rank of
|
|||
the expected answer in each completion request:
|
||||
|
||||
```console
|
||||
cargo r -r -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
cargo r --profile profiling -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
```
|
||||
|
||||
To debug a _specific_ task and look at the actual results, use the `show-one`
|
||||
|
@ -133,7 +133,7 @@ CI will also fail if the individual task results have changed.
|
|||
To make CI pass, you can just re-run the evaluation locally and commit the results:
|
||||
|
||||
```console
|
||||
cargo r -r -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
cargo r --profile profiling -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
```
|
||||
|
||||
CI fails in this case because it would be best to scrutinize the differences here.
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
name,file,index,rank
|
||||
fstring-completions,main.py,0,1
|
||||
higher-level-symbols-preferred,main.py,0,
|
||||
higher-level-symbols-preferred,main.py,1,1
|
||||
import-deprioritizes-dunder,main.py,0,195
|
||||
import-deprioritizes-sunder,main.py,0,195
|
||||
internal-typeshed-hidden,main.py,0,43
|
||||
import-deprioritizes-dunder,main.py,0,1
|
||||
import-deprioritizes-sunder,main.py,0,1
|
||||
internal-typeshed-hidden,main.py,0,4
|
||||
none-completion,main.py,0,11
|
||||
numpy-array,main.py,0,
|
||||
numpy-array,main.py,1,32
|
||||
object-attr-instance-methods,main.py,0,7
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
raise-uses-base-exception,main.py,0,42
|
||||
scope-existing-over-new-import,main.py,0,495
|
||||
scope-prioritize-closer,main.py,0,152
|
||||
scope-simple-long-identifier,main.py,0,140
|
||||
ty-extensions-lower-stdlib,main.py,0,142
|
||||
type-var-typing-over-ast,main.py,0,65
|
||||
type-var-typing-over-ast,main.py,1,353
|
||||
raise-uses-base-exception,main.py,0,2
|
||||
scope-existing-over-new-import,main.py,0,474
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
scope-simple-long-identifier,main.py,0,1
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,7
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,270
|
||||
|
|
|
|
@ -15,6 +15,9 @@ use regex::bytes::Regex;
|
|||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ty_ide::Completion;
|
||||
use ty_project::metadata::Options;
|
||||
use ty_project::metadata::options::EnvironmentOptions;
|
||||
use ty_project::metadata::value::RelativePathBuf;
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_python_semantic::ModuleName;
|
||||
|
||||
|
@ -117,8 +120,8 @@ impl ShowOneCommand {
|
|||
&& self
|
||||
.file_name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name == task.cursor_name())
|
||||
&& self.index.is_some_and(|index| index == task.cursor.index)
|
||||
.is_none_or(|name| name == task.cursor_name())
|
||||
&& self.index.is_none_or(|index| index == task.cursor.index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,6 +281,14 @@ impl Task {
|
|||
|
||||
let system = OsSystem::new(project_path);
|
||||
let mut project_metadata = ProjectMetadata::discover(project_path, &system)?;
|
||||
// Explicitly point ty to the .venv to avoid any set VIRTUAL_ENV variable to take precedence.
|
||||
project_metadata.apply_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python: Some(RelativePathBuf::cli(".venv")),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
let db = ProjectDatabase::new(project_metadata, system)?;
|
||||
Ok(Task {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[settings]
|
||||
auto-import = false
|
|
@ -0,0 +1,3 @@
|
|||
zqzqzq_identifier = 1
|
||||
|
||||
print(f"{zqzqzq_<CURSOR: zqzqzq_identifier>}")
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
8
crates/ty_completion_eval/truth/fstring-completions/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/fstring-completions/uv.lock
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
|
@ -0,0 +1,2 @@
|
|||
[settings]
|
||||
auto-import = false
|
1
crates/ty_completion_eval/truth/none-completion/main.py
Normal file
1
crates/ty_completion_eval/truth/none-completion/main.py
Normal file
|
@ -0,0 +1 @@
|
|||
x = Non<CURSOR: None>
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
8
crates/ty_completion_eval/truth/none-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/none-completion/uv.lock
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
|
@ -0,0 +1,2 @@
|
|||
[settings]
|
||||
auto-import = false
|
|
@ -0,0 +1,3 @@
|
|||
zqzqzq_identifier = 1
|
||||
|
||||
print(t"{zqzqzq_<CURSOR: zqzqzq_identifier>}")
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
8
crates/ty_completion_eval/truth/tstring-completions/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/tstring-completions/uv.lock
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
|
@ -7,7 +7,7 @@ use ruff_diagnostics::Edit;
|
|||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::{Token, TokenAt, TokenKind};
|
||||
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use ty_python_semantic::{
|
||||
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
|
||||
|
@ -18,6 +18,7 @@ use crate::docstring::Docstring;
|
|||
use crate::find_node::covering_node;
|
||||
use crate::goto::DefinitionsOrTargets;
|
||||
use crate::importer::{ImportRequest, Importer};
|
||||
use crate::symbols::QueryPattern;
|
||||
use crate::{Db, all_symbols};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -206,6 +207,15 @@ pub fn completion<'db>(
|
|||
offset: TextSize,
|
||||
) -> Vec<Completion<'db>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
if is_in_comment(&parsed, offset) || is_in_string(&parsed, offset) {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let typed = find_typed_text(db, file, &parsed, offset);
|
||||
let typed_query = typed
|
||||
.as_deref()
|
||||
.map(QueryPattern::new)
|
||||
.unwrap_or_else(QueryPattern::matches_all_symbols);
|
||||
|
||||
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
||||
return vec![];
|
||||
|
@ -235,12 +245,23 @@ pub fn completion<'db>(
|
|||
};
|
||||
let mut completions: Vec<Completion<'_>> = semantic_completions
|
||||
.into_iter()
|
||||
.filter(|c| typed_query.is_match_symbol_name(c.name.as_str()))
|
||||
.map(|c| Completion::from_semantic_completion(db, c))
|
||||
.collect();
|
||||
|
||||
if scoped.is_some() {
|
||||
add_keyword_value_completions(db, &typed_query, &mut completions);
|
||||
}
|
||||
if settings.auto_import {
|
||||
if let Some(scoped) = scoped {
|
||||
add_unimported_completions(db, file, &parsed, scoped, &mut completions);
|
||||
add_unimported_completions(
|
||||
db,
|
||||
file,
|
||||
&parsed,
|
||||
scoped,
|
||||
typed.as_deref(),
|
||||
&mut completions,
|
||||
);
|
||||
}
|
||||
}
|
||||
completions.sort_by(compare_suggestions);
|
||||
|
@ -248,6 +269,37 @@ pub fn completion<'db>(
|
|||
completions
|
||||
}
|
||||
|
||||
/// Adds a subset of completions derived from keywords.
|
||||
///
|
||||
/// Note that at present, these should only be added to "scoped"
|
||||
/// completions. i.e., This will include `None`, `True`, `False`, etc.
|
||||
fn add_keyword_value_completions<'db>(
|
||||
db: &'db dyn Db,
|
||||
query: &QueryPattern,
|
||||
completions: &mut Vec<Completion<'db>>,
|
||||
) {
|
||||
let keywords = [
|
||||
("None", Type::none(db)),
|
||||
("True", Type::BooleanLiteral(true)),
|
||||
("False", Type::BooleanLiteral(false)),
|
||||
];
|
||||
for (name, ty) in keywords {
|
||||
if !query.is_match_symbol_name(name) {
|
||||
continue;
|
||||
}
|
||||
completions.push(Completion {
|
||||
name: ast::name::Name::new(name),
|
||||
insert: None,
|
||||
ty: Some(ty),
|
||||
kind: None,
|
||||
module_name: None,
|
||||
import: None,
|
||||
builtin: true,
|
||||
documentation: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds completions not in scope.
|
||||
///
|
||||
/// `scoped` should be information about the identified scope
|
||||
|
@ -260,9 +312,10 @@ fn add_unimported_completions<'db>(
|
|||
file: File,
|
||||
parsed: &ParsedModuleRef,
|
||||
scoped: ScopedTarget<'_>,
|
||||
typed: Option<&str>,
|
||||
completions: &mut Vec<Completion<'db>>,
|
||||
) {
|
||||
let Some(typed) = scoped.typed else {
|
||||
let Some(typed) = typed else {
|
||||
return;
|
||||
};
|
||||
let source = source_text(db, file);
|
||||
|
@ -356,7 +409,7 @@ impl<'t> CompletionTargetTokens<'t> {
|
|||
TokenAt::Single(tok) => tok.end(),
|
||||
TokenAt::Between(_, tok) => tok.start(),
|
||||
};
|
||||
let before = parsed.tokens().before(offset);
|
||||
let before = tokens_start_before(parsed.tokens(), offset);
|
||||
Some(
|
||||
// Our strategy when it comes to `object.attribute` here is
|
||||
// to look for the `.` and then take the token immediately
|
||||
|
@ -485,21 +538,13 @@ impl<'t> CompletionTargetTokens<'t> {
|
|||
}
|
||||
CompletionTargetTokens::Generic { token } => {
|
||||
let node = covering_node(parsed.syntax().into(), token.range()).node();
|
||||
let typed = match node {
|
||||
ast::AnyNodeRef::ExprName(ast::ExprName { id, .. }) => {
|
||||
let name = id.as_str();
|
||||
if name.is_empty() { None } else { Some(name) }
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some(CompletionTargetAst::Scoped(ScopedTarget { node, typed }))
|
||||
Some(CompletionTargetAst::Scoped(ScopedTarget { node }))
|
||||
}
|
||||
CompletionTargetTokens::Unknown => {
|
||||
let range = TextRange::empty(offset);
|
||||
let covering_node = covering_node(parsed.syntax().into(), range);
|
||||
Some(CompletionTargetAst::Scoped(ScopedTarget {
|
||||
node: covering_node.node(),
|
||||
typed: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -561,11 +606,25 @@ struct ScopedTarget<'t> {
|
|||
/// The node with the smallest range that fully covers
|
||||
/// the token under the cursor.
|
||||
node: ast::AnyNodeRef<'t>,
|
||||
/// The text that has been typed so far, if available.
|
||||
///
|
||||
/// When not `None`, the typed text is guaranteed to be
|
||||
/// non-empty.
|
||||
typed: Option<&'t str>,
|
||||
}
|
||||
|
||||
/// Returns a slice of tokens that all start before or at the given
|
||||
/// [`TextSize`] offset.
|
||||
///
|
||||
/// If the given offset is between two tokens, the returned slice will end just
|
||||
/// before the following token. In other words, if the offset is between the
|
||||
/// end of previous token and start of next token, the returned slice will end
|
||||
/// just before the next token.
|
||||
///
|
||||
/// Unlike `Tokens::before`, this never panics. If `offset` is within a token's
|
||||
/// range (including if it's at the very beginning), then that token will be
|
||||
/// included in the slice returned.
|
||||
fn tokens_start_before(tokens: &Tokens, offset: TextSize) -> &[Token] {
|
||||
let idx = match tokens.binary_search_by(|token| token.start().cmp(&offset)) {
|
||||
Ok(idx) => idx,
|
||||
Err(idx) => idx,
|
||||
};
|
||||
&tokens[..idx]
|
||||
}
|
||||
|
||||
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
|
||||
|
@ -729,6 +788,57 @@ fn import_tokens(tokens: &[Token]) -> Option<(&Token, &Token)> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Looks for the text typed immediately before the cursor offset
|
||||
/// given.
|
||||
///
|
||||
/// If there isn't any typed text or it could not otherwise be found,
|
||||
/// then `None` is returned.
|
||||
fn find_typed_text(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
parsed: &ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
) -> Option<String> {
|
||||
let source = source_text(db, file);
|
||||
let tokens = tokens_start_before(parsed.tokens(), offset);
|
||||
let last = tokens.last()?;
|
||||
if !matches!(last.kind(), TokenKind::Name) {
|
||||
return None;
|
||||
}
|
||||
// This one's weird, but if the cursor is beyond
|
||||
// what is in the closest `Name` token, then it's
|
||||
// likely we can't infer anything about what has
|
||||
// been typed. This likely means there is whitespace
|
||||
// or something that isn't represented in the token
|
||||
// stream. So just give up.
|
||||
if last.end() < offset {
|
||||
return None;
|
||||
}
|
||||
Some(source[last.range()].to_string())
|
||||
}
|
||||
|
||||
/// Whether the given offset within the parsed module is within
|
||||
/// a comment or not.
|
||||
fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
|
||||
let tokens = tokens_start_before(parsed.tokens(), offset);
|
||||
tokens.last().is_some_and(|t| t.kind().is_comment())
|
||||
}
|
||||
|
||||
/// Returns true when the cursor at `offset` is positioned within
|
||||
/// a string token (regular, f-string, t-string, etc).
|
||||
///
|
||||
/// Note that this will return `false` when positioned within an
|
||||
/// interpolation block in an f-string or a t-string.
|
||||
fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
|
||||
let tokens = tokens_start_before(parsed.tokens(), offset);
|
||||
tokens.last().is_some_and(|t| {
|
||||
matches!(
|
||||
t.kind(),
|
||||
TokenKind::String | TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Order completions lexicographically, with these exceptions:
|
||||
///
|
||||
/// 1) A `_[^_]` prefix sorts last and
|
||||
|
@ -1055,7 +1165,7 @@ g<CURSOR>
|
|||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @"foo");
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found after filtering out completions>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1493,10 +1603,8 @@ class Foo:
|
|||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
Foo
|
||||
bar
|
||||
frob
|
||||
quux
|
||||
");
|
||||
}
|
||||
|
||||
|
@ -1510,11 +1618,7 @@ class Foo:
|
|||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
Foo
|
||||
bar
|
||||
quux
|
||||
");
|
||||
assert_snapshot!(test.completions_without_builtins(), @"bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1687,29 +1791,8 @@ quux.b<CURSOR>
|
|||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
bar :: Unknown | Literal[2]
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type[Quux]
|
||||
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method Quux.__getstate__() -> object
|
||||
__hash__ :: bound method Quux.__hash__() -> int
|
||||
__init__ :: bound method Quux.__init__() -> Unknown
|
||||
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method Quux.__new__() -> Quux
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method Quux.__sizeof__() -> int
|
||||
__str__ :: bound method Quux.__str__() -> str
|
||||
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
||||
");
|
||||
}
|
||||
|
@ -2059,10 +2142,7 @@ bar(o<CURSOR>
|
|||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
bar
|
||||
foo
|
||||
");
|
||||
assert_snapshot!(test.completions_without_builtins(), @"foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -2097,8 +2177,6 @@ class C:
|
|||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
C
|
||||
bar
|
||||
foo
|
||||
self
|
||||
");
|
||||
|
@ -2133,8 +2211,6 @@ class C:
|
|||
// that is only a method that can be called on
|
||||
// `self`.
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
C
|
||||
bar
|
||||
foo
|
||||
self
|
||||
");
|
||||
|
@ -2179,7 +2255,7 @@ hidden_<CURSOR>
|
|||
|
||||
assert_snapshot!(
|
||||
test.completions_without_builtins(),
|
||||
@"<No completions found after filtering out completions>",
|
||||
@"<No completions found>",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2199,7 +2275,10 @@ if sys.platform == \"not-my-current-platform\":
|
|||
// TODO: ideally, `only_available_in_this_branch` should be available here, but we
|
||||
// currently make no effort to provide a good IDE experience within sections that
|
||||
// are unreachable
|
||||
assert_snapshot!(test.completions_without_builtins(), @"sys");
|
||||
assert_snapshot!(
|
||||
test.completions_without_builtins(),
|
||||
@"<No completions found after filtering out completions>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -2785,17 +2864,7 @@ f = Foo()
|
|||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should not have any completions suggested for it.
|
||||
// We do correctly avoid giving `object.attr` completions here,
|
||||
// but we instead fall back to scope based completions. Since
|
||||
// we're inside a string, we should avoid giving completions at
|
||||
// all.
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
Foo
|
||||
bar
|
||||
f
|
||||
foo
|
||||
");
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -2818,6 +2887,26 @@ f"{f.<CURSOR>
|
|||
test.assert_completions_include("method");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_dot_attr3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
foo = 1
|
||||
bar = 2
|
||||
|
||||
class Foo:
|
||||
def method(self): ...
|
||||
|
||||
f = Foo()
|
||||
|
||||
# T-string, this is an attribute access
|
||||
t"{f.<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.assert_completions_include("method");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_panic_for_attribute_table_that_contains_subscript() {
|
||||
let test = cursor_test(
|
||||
|
@ -3315,6 +3404,498 @@ from os.<CURSOR>
|
|||
assert_eq!(completion.kind(&test.db), Some(CompletionKind::Struct));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_comment() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
# zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(\"zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(\"Foo.zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_incomplete_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(\"Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print('zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print('Foo.zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_incomplete_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print('zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print('Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(\"\"\"zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(\"\"\"Foo.zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_incomplete_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(\"\"\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(\"\"\"Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print('''zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print('''Foo.zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_string_incomplete_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print('''zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print('''Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f\"zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f\"{Foo} and Foo.zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_incomplete_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f\"{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f'zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f'{Foo} and Foo.zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_incomplete_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f'zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f'{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f\"\"\"zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f\"\"\"{Foo} and Foo.zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_incomplete_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f\"\"\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f\"\"\"{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f'''zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f'''{Foo} and Foo.zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_fstring_incomplete_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(f'''zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(f'''{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t\"zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t\"{Foo} and Foo.zqzq<CURSOR>\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_incomplete_double_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t\"{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t'zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t'{Foo} and Foo.zqzq<CURSOR>')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_incomplete_single_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t'zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t'{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t\"\"\"zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t\"\"\"{Foo} and Foo.zqzq<CURSOR>\"\"\")
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_incomplete_double_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t\"\"\"zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t\"\"\"{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t'''zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t'''{Foo} and Foo.zqzq<CURSOR>''')
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_tstring_incomplete_single_triple_quote() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
zqzqzq = 1
|
||||
print(t'''zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Foo:
|
||||
zqzqzq = 1
|
||||
print(t'''{Foo} and Foo.zqzq<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
// NOTE: The methods below are getting somewhat ridiculous.
|
||||
// We should refactor this by converting to using a builder
|
||||
// to set different modes. ---AG
|
||||
|
|
|
@ -44,17 +44,26 @@ impl QueryPattern {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_match(&self, symbol: &SymbolInfo<'_>) -> bool {
|
||||
/// Create a new query pattern that matches all symbols.
|
||||
pub fn matches_all_symbols() -> QueryPattern {
|
||||
QueryPattern {
|
||||
re: None,
|
||||
original: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_match_symbol(&self, symbol: &SymbolInfo<'_>) -> bool {
|
||||
self.is_match_symbol_name(&symbol.name)
|
||||
}
|
||||
|
||||
fn is_match_symbol_name(&self, symbol_name: &str) -> bool {
|
||||
pub fn is_match_symbol_name(&self, symbol_name: &str) -> bool {
|
||||
if let Some(ref re) = self.re {
|
||||
re.is_match(symbol_name)
|
||||
} else {
|
||||
// This is a degenerate case. The only way
|
||||
// we should get here is if the query string
|
||||
// was thousands (or more) characters long.
|
||||
// ... or, if "typed" text could not be found.
|
||||
symbol_name.contains(&self.original)
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +117,8 @@ impl FlatSymbols {
|
|||
|
||||
/// Returns a sequence of symbols that matches the given query.
|
||||
pub fn search(&self, query: &QueryPattern) -> impl Iterator<Item = (SymbolId, SymbolInfo<'_>)> {
|
||||
self.iter().filter(|(_, symbol)| query.is_match(symbol))
|
||||
self.iter()
|
||||
.filter(|(_, symbol)| query.is_match_symbol(symbol))
|
||||
}
|
||||
|
||||
/// Turns this flat sequence of symbols into a hierarchy of symbols.
|
||||
|
|
|
@ -6253,7 +6253,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
|
||||
/// The type `NoneType` / `None`
|
||||
pub(crate) fn none(db: &'db dyn Db) -> Type<'db> {
|
||||
pub fn none(db: &'db dyn Db) -> Type<'db> {
|
||||
KnownClass::NoneType.to_instance(db)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ use std::time::Instant;
|
|||
|
||||
use lsp_types::request::Completion;
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
|
||||
CompletionResponse, Documentation, TextEdit, Url,
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList,
|
||||
CompletionParams, CompletionResponse, Documentation, TextEdit, Url,
|
||||
};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
@ -100,7 +100,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
|||
})
|
||||
.collect();
|
||||
let len = items.len();
|
||||
let response = CompletionResponse::Array(items);
|
||||
let response = CompletionResponse::List(CompletionList {
|
||||
is_incomplete: true,
|
||||
items,
|
||||
});
|
||||
tracing::debug!(
|
||||
"Completions request returned {len} suggestions in {elapsed:?}",
|
||||
elapsed = Instant::now().duration_since(start)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue