From ed32f765bb0ac4d72984b8cbaf4e32c0e0062766 Mon Sep 17 00:00:00 2001 From: Rasmus Nygren Date: Sat, 22 Nov 2025 12:40:38 +0100 Subject: [PATCH 1/2] [ty] Only suggest completions based on text before the cursor Previously we extracted the entire token as the query independently of the cursor position. By not doing that you avoid having to do special range handling to figure out the start position of the current token. It's likely also more intuitive from a user perspective to only consider characters left of the cursor when suggesting autocompletions. --- crates/ty_ide/src/completion.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 26175462a3d32..29f4992c05829 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1315,7 +1315,8 @@ fn find_typed_text( if last.end() < offset || last.range().is_empty() { return None; } - Some(source[last.range()].to_string()) + let range = TextRange::new(last.start(), offset); + Some(source[range].to_string()) } /// Whether the last token is in a place where we should not provide completions. @@ -1633,6 +1634,21 @@ mod tests { ); } + #[test] + fn inside_token() { + let test = completion_test_builder( + "\ +foo_bar_baz = 1 +x = foobad +", + ); + + assert_snapshot!( + test.skip_builtins().build().snapshot(), + @"foo_bar_baz", + ); + } + #[test] fn type_keyword_dedup() { let test = completion_test_builder( From 2b542275f06d5612b56f839133ecf7435d110f1b Mon Sep 17 00:00:00 2001 From: Rasmus Nygren Date: Sat, 22 Nov 2025 12:40:51 +0100 Subject: [PATCH 2/2] [ty] Improve autocomplete suppressions of keywords in variable bindings Autocomplete suggestions were not suppressed correctly during some variable bindings if the parameter name was currently matching a keyword. E.g. `def f(foo` was handled correctly but not `def f(in`. --- crates/ty_ide/src/completion.rs | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 29f4992c05829..03ef65f14a802 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1407,6 +1407,24 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt type_param.name.range.contains_range(range) } ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range), + // The AST does not produce `ast::AnyNodeRef::Parameter` nodes for keywords + // or otherwise invalid syntax. Rather they are captured in a + // `ast::AnyNodeRef::Parameters` node as "empty space". To ensure + // we still suppress suggestions even when the syntax is technically + // invalid we extract the token under the cursor and check if it makes + // up that "empty space" inside the Parameters Node. If it does, we know + // that we are still binding variables, just that the current state is + // syntatically invalid. Hence we suppress autocomplete suggestons + // also in those cases. + ast::AnyNodeRef::Parameters(params) => { + if !params.range.contains_range(range) { + return false; + } + params + .iter() + .map(|param| param.range()) + .all(|r| !r.contains_range(range)) + } _ => false, }) } @@ -5363,6 +5381,45 @@ def foo(p ); } + #[test] + fn no_completions_in_function_param_keyword() { + let builder = completion_test_builder( + "\ +def foo(in +", + ); + assert_snapshot!( + builder.build().snapshot(), + @"", + ); + } + + #[test] + fn no_completions_in_function_param_multi_keyword() { + let builder = completion_test_builder( + "\ +def foo(param, in +", + ); + assert_snapshot!( + builder.build().snapshot(), + @"", + ); + } + + #[test] + fn no_completions_in_function_param_multi_keyword_middle() { + let builder = completion_test_builder( + "\ +def foo(param, in, param_two +", + ); + assert_snapshot!( + builder.build().snapshot(), + @"", + ); + } + #[test] fn no_completions_in_function_type_param() { let builder = completion_test_builder(