[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:
Andrew Gallant 2025-10-15 04:59:33 -04:00 committed by GitHub
parent 4fc7dd300c
commit 651f7963a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 753 additions and 93 deletions

View File

@ -721,7 +721,7 @@ jobs:
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Run ty completion evaluation" - 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" - name: "Ensure there are no changes"
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv

View File

@ -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: `all` command from the root of this repository:
```console ```console
cargo run --release --package ty_completion_eval -- all cargo run --profile profiling --package ty_completion_eval -- all
``` ```
The output should look like this: 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: the expected answer in each completion request:
```console ```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` 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: To make CI pass, you can just re-run the evaluation locally and commit the results:
```console ```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. CI fails in this case because it would be best to scrutinize the differences here.

View File

@ -1,17 +1,20 @@
name,file,index,rank name,file,index,rank
fstring-completions,main.py,0,1
higher-level-symbols-preferred,main.py,0, higher-level-symbols-preferred,main.py,0,
higher-level-symbols-preferred,main.py,1,1 higher-level-symbols-preferred,main.py,1,1
import-deprioritizes-dunder,main.py,0,195 import-deprioritizes-dunder,main.py,0,1
import-deprioritizes-sunder,main.py,0,195 import-deprioritizes-sunder,main.py,0,1
internal-typeshed-hidden,main.py,0,43 internal-typeshed-hidden,main.py,0,4
none-completion,main.py,0,11
numpy-array,main.py,0, numpy-array,main.py,0,
numpy-array,main.py,1,32 numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,7 object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1 object-attr-instance-methods,main.py,1,1
raise-uses-base-exception,main.py,0,42 raise-uses-base-exception,main.py,0,2
scope-existing-over-new-import,main.py,0,495 scope-existing-over-new-import,main.py,0,474
scope-prioritize-closer,main.py,0,152 scope-prioritize-closer,main.py,0,2
scope-simple-long-identifier,main.py,0,140 scope-simple-long-identifier,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,142 tstring-completions,main.py,0,1
type-var-typing-over-ast,main.py,0,65 ty-extensions-lower-stdlib,main.py,0,7
type-var-typing-over-ast,main.py,1,353 type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,270

1 name file index rank
2 fstring-completions main.py 0 1
3 higher-level-symbols-preferred main.py 0
4 higher-level-symbols-preferred main.py 1 1
5 import-deprioritizes-dunder main.py 0 195 1
6 import-deprioritizes-sunder main.py 0 195 1
7 internal-typeshed-hidden main.py 0 43 4
8 none-completion main.py 0 11
9 numpy-array main.py 0
10 numpy-array main.py 1 32 1
11 object-attr-instance-methods main.py 0 7 1
12 object-attr-instance-methods main.py 1 1
13 raise-uses-base-exception main.py 0 42 2
14 scope-existing-over-new-import main.py 0 495 474
15 scope-prioritize-closer main.py 0 152 2
16 scope-simple-long-identifier main.py 0 140 1
17 ty-extensions-lower-stdlib tstring-completions main.py 0 142 1
18 type-var-typing-over-ast ty-extensions-lower-stdlib main.py 0 65 7
19 type-var-typing-over-ast main.py 1 0 353 3
20 type-var-typing-over-ast main.py 1 270

View File

@ -15,6 +15,9 @@ use regex::bytes::Regex;
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ty_ide::Completion; 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_project::{ProjectDatabase, ProjectMetadata};
use ty_python_semantic::ModuleName; use ty_python_semantic::ModuleName;
@ -117,8 +120,8 @@ impl ShowOneCommand {
&& self && self
.file_name .file_name
.as_ref() .as_ref()
.is_some_and(|name| name == task.cursor_name()) .is_none_or(|name| name == task.cursor_name())
&& self.index.is_some_and(|index| index == task.cursor.index) && self.index.is_none_or(|index| index == task.cursor.index)
} }
} }
@ -278,6 +281,14 @@ impl Task {
let system = OsSystem::new(project_path); let system = OsSystem::new(project_path);
let mut project_metadata = ProjectMetadata::discover(project_path, &system)?; 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)?; project_metadata.apply_configuration_files(&system)?;
let db = ProjectDatabase::new(project_metadata, system)?; let db = ProjectDatabase::new(project_metadata, system)?;
Ok(Task { Ok(Task {

View File

@ -0,0 +1,2 @@
[settings]
auto-import = false

View File

@ -0,0 +1,3 @@
zqzqzq_identifier = 1
print(f"{zqzqzq_<CURSOR: zqzqzq_identifier>}")

View File

@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@ -0,0 +1,2 @@
[settings]
auto-import = false

View File

@ -0,0 +1 @@
x = Non<CURSOR: None>

View File

@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@ -0,0 +1,2 @@
[settings]
auto-import = false

View File

@ -0,0 +1,3 @@
zqzqzq_identifier = 1
print(t"{zqzqzq_<CURSOR: zqzqzq_identifier>}")

View File

@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@ -7,7 +7,7 @@ use ruff_diagnostics::Edit;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_codegen::Stylist; 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 ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::{ use ty_python_semantic::{
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel, Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
@ -18,6 +18,7 @@ use crate::docstring::Docstring;
use crate::find_node::covering_node; use crate::find_node::covering_node;
use crate::goto::DefinitionsOrTargets; use crate::goto::DefinitionsOrTargets;
use crate::importer::{ImportRequest, Importer}; use crate::importer::{ImportRequest, Importer};
use crate::symbols::QueryPattern;
use crate::{Db, all_symbols}; use crate::{Db, all_symbols};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -206,6 +207,15 @@ pub fn completion<'db>(
offset: TextSize, offset: TextSize,
) -> Vec<Completion<'db>> { ) -> Vec<Completion<'db>> {
let parsed = parsed_module(db, file).load(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 { let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
return vec![]; return vec![];
@ -235,12 +245,23 @@ pub fn completion<'db>(
}; };
let mut completions: Vec<Completion<'_>> = semantic_completions let mut completions: Vec<Completion<'_>> = semantic_completions
.into_iter() .into_iter()
.filter(|c| typed_query.is_match_symbol_name(c.name.as_str()))
.map(|c| Completion::from_semantic_completion(db, c)) .map(|c| Completion::from_semantic_completion(db, c))
.collect(); .collect();
if scoped.is_some() {
add_keyword_value_completions(db, &typed_query, &mut completions);
}
if settings.auto_import { if settings.auto_import {
if let Some(scoped) = scoped { 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); completions.sort_by(compare_suggestions);
@ -248,6 +269,37 @@ pub fn completion<'db>(
completions 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. /// Adds completions not in scope.
/// ///
/// `scoped` should be information about the identified scope /// `scoped` should be information about the identified scope
@ -260,9 +312,10 @@ fn add_unimported_completions<'db>(
file: File, file: File,
parsed: &ParsedModuleRef, parsed: &ParsedModuleRef,
scoped: ScopedTarget<'_>, scoped: ScopedTarget<'_>,
typed: Option<&str>,
completions: &mut Vec<Completion<'db>>, completions: &mut Vec<Completion<'db>>,
) { ) {
let Some(typed) = scoped.typed else { let Some(typed) = typed else {
return; return;
}; };
let source = source_text(db, file); let source = source_text(db, file);
@ -356,7 +409,7 @@ impl<'t> CompletionTargetTokens<'t> {
TokenAt::Single(tok) => tok.end(), TokenAt::Single(tok) => tok.end(),
TokenAt::Between(_, tok) => tok.start(), TokenAt::Between(_, tok) => tok.start(),
}; };
let before = parsed.tokens().before(offset); let before = tokens_start_before(parsed.tokens(), offset);
Some( Some(
// Our strategy when it comes to `object.attribute` here is // Our strategy when it comes to `object.attribute` here is
// to look for the `.` and then take the token immediately // to look for the `.` and then take the token immediately
@ -485,21 +538,13 @@ impl<'t> CompletionTargetTokens<'t> {
} }
CompletionTargetTokens::Generic { token } => { CompletionTargetTokens::Generic { token } => {
let node = covering_node(parsed.syntax().into(), token.range()).node(); let node = covering_node(parsed.syntax().into(), token.range()).node();
let typed = match node { Some(CompletionTargetAst::Scoped(ScopedTarget { 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 }))
} }
CompletionTargetTokens::Unknown => { CompletionTargetTokens::Unknown => {
let range = TextRange::empty(offset); let range = TextRange::empty(offset);
let covering_node = covering_node(parsed.syntax().into(), range); let covering_node = covering_node(parsed.syntax().into(), range);
Some(CompletionTargetAst::Scoped(ScopedTarget { Some(CompletionTargetAst::Scoped(ScopedTarget {
node: covering_node.node(), node: covering_node.node(),
typed: None,
})) }))
} }
} }
@ -561,11 +606,25 @@ struct ScopedTarget<'t> {
/// The node with the smallest range that fully covers /// The node with the smallest range that fully covers
/// the token under the cursor. /// the token under the cursor.
node: ast::AnyNodeRef<'t>, node: ast::AnyNodeRef<'t>,
/// The text that has been typed so far, if available. }
///
/// When not `None`, the typed text is guaranteed to be /// Returns a slice of tokens that all start before or at the given
/// non-empty. /// [`TextSize`] offset.
typed: Option<&'t str>, ///
/// 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. /// Returns a suffix of `tokens` corresponding to the `kinds` given.
@ -729,6 +788,57 @@ fn import_tokens(tokens: &[Token]) -> Option<(&Token, &Token)> {
None 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: /// Order completions lexicographically, with these exceptions:
/// ///
/// 1) A `_[^_]` prefix sorts last and /// 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] #[test]
@ -1493,10 +1603,8 @@ class Foo:
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins(), @r"
Foo
bar bar
frob frob
quux
"); ");
} }
@ -1510,11 +1618,7 @@ class Foo:
", ",
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins(), @"bar");
Foo
bar
quux
");
} }
#[test] #[test]
@ -1687,29 +1791,8 @@ quux.b<CURSOR>
assert_snapshot!(test.completions_without_builtins_with_types(), @r" assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar :: Unknown | Literal[2] bar :: Unknown | Literal[2]
baz :: Unknown | Literal[3] 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 __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 __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 __subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
"); ");
} }
@ -2059,10 +2142,7 @@ bar(o<CURSOR>
", ",
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins(), @"foo");
bar
foo
");
} }
#[test] #[test]
@ -2097,8 +2177,6 @@ class C:
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins(), @r"
C
bar
foo foo
self self
"); ");
@ -2133,8 +2211,6 @@ class C:
// that is only a method that can be called on // that is only a method that can be called on
// `self`. // `self`.
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins(), @r"
C
bar
foo foo
self self
"); ");
@ -2179,7 +2255,7 @@ hidden_<CURSOR>
assert_snapshot!( assert_snapshot!(
test.completions_without_builtins(), 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 // 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 // currently make no effort to provide a good IDE experience within sections that
// are unreachable // are unreachable
assert_snapshot!(test.completions_without_builtins(), @"sys"); assert_snapshot!(
test.completions_without_builtins(),
@"<No completions found after filtering out completions>",
);
} }
#[test] #[test]
@ -2785,17 +2864,7 @@ f = Foo()
"#, "#,
); );
// TODO: This should not have any completions suggested for it. assert_snapshot!(test.completions_without_builtins(), @r"<No completions found>");
// 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
");
} }
#[test] #[test]
@ -2818,6 +2887,26 @@ f"{f.<CURSOR>
test.assert_completions_include("method"); 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] #[test]
fn no_panic_for_attribute_table_that_contains_subscript() { fn no_panic_for_attribute_table_that_contains_subscript() {
let test = cursor_test( let test = cursor_test(
@ -3315,6 +3404,498 @@ from os.<CURSOR>
assert_eq!(completion.kind(&test.db), Some(CompletionKind::Struct)); 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. // NOTE: The methods below are getting somewhat ridiculous.
// We should refactor this by converting to using a builder // We should refactor this by converting to using a builder
// to set different modes. ---AG // to set different modes. ---AG

View File

@ -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) 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 { if let Some(ref re) = self.re {
re.is_match(symbol_name) re.is_match(symbol_name)
} else { } else {
// This is a degenerate case. The only way // This is a degenerate case. The only way
// we should get here is if the query string // we should get here is if the query string
// was thousands (or more) characters long. // was thousands (or more) characters long.
// ... or, if "typed" text could not be found.
symbol_name.contains(&self.original) symbol_name.contains(&self.original)
} }
} }
@ -108,7 +117,8 @@ impl FlatSymbols {
/// Returns a sequence of symbols that matches the given query. /// Returns a sequence of symbols that matches the given query.
pub fn search(&self, query: &QueryPattern) -> impl Iterator<Item = (SymbolId, SymbolInfo<'_>)> { 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. /// Turns this flat sequence of symbols into a hierarchy of symbols.

View File

@ -6253,7 +6253,7 @@ impl<'db> Type<'db> {
} }
/// The type `NoneType` / `None` /// 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) KnownClass::NoneType.to_instance(db)
} }

View File

@ -3,8 +3,8 @@ use std::time::Instant;
use lsp_types::request::Completion; use lsp_types::request::Completion;
use lsp_types::{ use lsp_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList,
CompletionResponse, Documentation, TextEdit, Url, CompletionParams, CompletionResponse, Documentation, TextEdit, Url,
}; };
use ruff_db::source::{line_index, source_text}; use ruff_db::source::{line_index, source_text};
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
@ -100,7 +100,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
}) })
.collect(); .collect();
let len = items.len(); let len = items.len();
let response = CompletionResponse::Array(items); let response = CompletionResponse::List(CompletionList {
is_incomplete: true,
items,
});
tracing::debug!( tracing::debug!(
"Completions request returned {len} suggestions in {elapsed:?}", "Completions request returned {len} suggestions in {elapsed:?}",
elapsed = Instant::now().duration_since(start) elapsed = Instant::now().duration_since(start)