diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 78e22a12d0..d8606cd463 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/crates/ty_completion_eval/README.md b/crates/ty_completion_eval/README.md index 0927c354e4..748c6e9d0d 100644 --- a/crates/ty_completion_eval/README.md +++ b/crates/ty_completion_eval/README.md @@ -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. diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 03f14a8a66..00b612e217 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -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 diff --git a/crates/ty_completion_eval/src/main.rs b/crates/ty_completion_eval/src/main.rs index 784a141de7..146041a278 100644 --- a/crates/ty_completion_eval/src/main.rs +++ b/crates/ty_completion_eval/src/main.rs @@ -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 { diff --git a/crates/ty_completion_eval/truth/fstring-completions/completion.toml b/crates/ty_completion_eval/truth/fstring-completions/completion.toml new file mode 100644 index 0000000000..1c3c4b8ea4 --- /dev/null +++ b/crates/ty_completion_eval/truth/fstring-completions/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = false diff --git a/crates/ty_completion_eval/truth/fstring-completions/main.py b/crates/ty_completion_eval/truth/fstring-completions/main.py new file mode 100644 index 0000000000..767245df1e --- /dev/null +++ b/crates/ty_completion_eval/truth/fstring-completions/main.py @@ -0,0 +1,3 @@ +zqzqzq_identifier = 1 + +print(f"{zqzqzq_}") diff --git a/crates/ty_completion_eval/truth/fstring-completions/pyproject.toml b/crates/ty_completion_eval/truth/fstring-completions/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/fstring-completions/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/fstring-completions/uv.lock b/crates/ty_completion_eval/truth/fstring-completions/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/fstring-completions/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_completion_eval/truth/none-completion/completion.toml b/crates/ty_completion_eval/truth/none-completion/completion.toml new file mode 100644 index 0000000000..1c3c4b8ea4 --- /dev/null +++ b/crates/ty_completion_eval/truth/none-completion/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = false diff --git a/crates/ty_completion_eval/truth/none-completion/main.py b/crates/ty_completion_eval/truth/none-completion/main.py new file mode 100644 index 0000000000..4f5674c1ee --- /dev/null +++ b/crates/ty_completion_eval/truth/none-completion/main.py @@ -0,0 +1 @@ +x = Non diff --git a/crates/ty_completion_eval/truth/none-completion/pyproject.toml b/crates/ty_completion_eval/truth/none-completion/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/none-completion/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/none-completion/uv.lock b/crates/ty_completion_eval/truth/none-completion/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/none-completion/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_completion_eval/truth/tstring-completions/completion.toml b/crates/ty_completion_eval/truth/tstring-completions/completion.toml new file mode 100644 index 0000000000..1c3c4b8ea4 --- /dev/null +++ b/crates/ty_completion_eval/truth/tstring-completions/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = false diff --git a/crates/ty_completion_eval/truth/tstring-completions/main.py b/crates/ty_completion_eval/truth/tstring-completions/main.py new file mode 100644 index 0000000000..01f65e4536 --- /dev/null +++ b/crates/ty_completion_eval/truth/tstring-completions/main.py @@ -0,0 +1,3 @@ +zqzqzq_identifier = 1 + +print(t"{zqzqzq_}") diff --git a/crates/ty_completion_eval/truth/tstring-completions/pyproject.toml b/crates/ty_completion_eval/truth/tstring-completions/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/tstring-completions/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/tstring-completions/uv.lock b/crates/ty_completion_eval/truth/tstring-completions/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/tstring-completions/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 2cec00791e..e8405c863d 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -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> { 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> = 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>, +) { + 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>, ) { - 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 { + 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 ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(test.completions_without_builtins(), @""); } #[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 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 ", ); - 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_ assert_snapshot!( test.completions_without_builtins(), - @"", + @"", ); } @@ -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(), + @"", + ); } #[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""); } #[test] @@ -2818,6 +2887,26 @@ f"{f. 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. +"#, + ); + + 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. assert_eq!(completion.kind(&test.db), Some(CompletionKind::Struct)); } + #[test] + fn no_completions_in_comment() { + let test = cursor_test( + "\ +zqzqzq = 1 +# zqzq +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(\"zqzq\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(\"Foo.zqzq\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_incomplete_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(\"zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(\"Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print('zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print('Foo.zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_incomplete_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print('zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print('Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(\"\"\"zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(\"\"\"Foo.zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_incomplete_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(\"\"\"zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(\"\"\"Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print('''zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print('''Foo.zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_string_incomplete_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print('''zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print('''Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f\"zqzq\") + ", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f\"{Foo} and Foo.zqzq\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_incomplete_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f\"zqzq + ", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f\"{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f'zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f'{Foo} and Foo.zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_incomplete_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f'zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f'{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f\"\"\"zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f\"\"\"{Foo} and Foo.zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_incomplete_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f\"\"\"zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f\"\"\"{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f'''zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f'''{Foo} and Foo.zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_fstring_incomplete_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(f'''zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(f'''{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t\"zqzq\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t\"{Foo} and Foo.zqzq\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_incomplete_double_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t\"zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t\"{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t'zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t'{Foo} and Foo.zqzq') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_incomplete_single_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t'zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t'{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t\"\"\"zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t\"\"\"{Foo} and Foo.zqzq\"\"\") +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_incomplete_double_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t\"\"\"zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t\"\"\"{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t'''zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t'''{Foo} and Foo.zqzq''') +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn no_completions_in_tstring_incomplete_single_triple_quote() { + let test = cursor_test( + "\ +zqzqzq = 1 +print(t'''zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + + let test = cursor_test( + "\ +class Foo: + zqzqzq = 1 +print(t'''{Foo} and Foo.zqzq +", + ); + assert_snapshot!(test.completions_without_builtins(), @""); + } + // NOTE: The methods below are getting somewhat ridiculous. // We should refactor this by converting to using a builder // to set different modes. ---AG diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 39cc0c3aea..a5af219b98 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -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)> { - 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. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7f9d182c69..777a2f2ee8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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) } diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index 55b8b91074..35e1c3fd25 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -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)