diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 2c57afef2f..35b85b0d09 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -3136,11 +3136,17 @@ impl<'a> Transaction<'a> { supports_completion_item_details, ); } - // If autoimport completions were skipped due to character threshold, - // mark the results as incomplete so clients keep asking for completions. - // This ensures autoimport completions will be checked once the threshold is reached, - // even if local completions are currently available. - if identifier.as_str().len() < MIN_CHARACTERS_TYPED_AUTOIMPORT { + // Mark results as incomplete in the following cases so clients keep asking + // for completions as the user types more: + // 1. If identifier is below MIN_CHARACTERS_TYPED_AUTOIMPORT threshold, + // autoimport completions are skipped and will be checked once threshold + // is reached. + // 2. If local completions exist and blocked autoimport completions, + // the local completions might not match as the user continues typing, + // and autoimport completions should then be shown. + if identifier.as_str().len() < MIN_CHARACTERS_TYPED_AUTOIMPORT + || has_local_completions + { is_incomplete = true; } self.add_builtins_autoimport_completions(handle, Some(&identifier), &mut result); diff --git a/pyrefly/lib/test/lsp/lsp_interaction/completion.rs b/pyrefly/lib/test/lsp/lsp_interaction/completion.rs index 51118f465a..8f711e3ef1 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/completion.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/completion.rs @@ -602,3 +602,78 @@ fn test_completion_complete_with_local_completions() { interaction.shutdown().unwrap(); } + +#[test] +fn test_completion_incomplete_with_local_completions_blocking_autoimport() { + let root = get_test_files_root(); + let mut interaction = LspInteraction::new(); + interaction.set_root(root.path().join("autoimport_common_prefix")); + interaction + .initialize(InitializeSettings::default()) + .unwrap(); + + // Open b.py which has UsersController, and a.py which has UsersManager + interaction.client.did_open("b.py"); + interaction.client.did_open("a.py"); + + // Type "Users" (5 characters, above MIN_CHARACTERS_TYPED_AUTOIMPORT = 3) + // in b.py. Local completion UsersController exists, so autoimport is skipped. + // But is_incomplete should still be true because the local completion might + // not match as the user continues typing (e.g., "UsersM" should show UsersManager). + interaction + .client + .did_change("b.py", "class UsersController:\n pass\n\nUsers"); + + interaction + .client + .completion("b.py", 3, 5) + .expect_completion_response_with(|list| { + // Should have local completion UsersController + let has_users_controller = list + .items + .iter() + .any(|item| item.label == "UsersController"); + // is_incomplete should be true so client asks again when typing more + has_users_controller && list.is_incomplete + }) + .unwrap(); + + interaction.shutdown().unwrap(); +} + +#[test] +fn test_completion_autoimport_shown_when_local_no_longer_matches() { + let root = get_test_files_root(); + let mut interaction = LspInteraction::new(); + interaction.set_root(root.path().join("autoimport_common_prefix")); + interaction + .initialize(InitializeSettings::default()) + .unwrap(); + + // Open b.py which has UsersController, and a.py which has UsersManager + interaction.client.did_open("b.py"); + interaction.client.did_open("a.py"); + + // Type "UsersM" - this should NOT match local "UsersController" (no 'M' in it) + // but SHOULD match autoimport "UsersManager" from a.py + interaction + .client + .did_change("b.py", "class UsersController:\n pass\n\nUsersM"); + + interaction + .client + .completion("b.py", 3, 6) + .expect_completion_response_with(|list| { + // Should have autoimport completion UsersManager + let has_users_manager = list.items.iter().any(|item| item.label == "UsersManager"); + // Should NOT have UsersController (doesn't match "UsersM") + let has_users_controller = list + .items + .iter() + .any(|item| item.label == "UsersController"); + has_users_manager && !has_users_controller + }) + .unwrap(); + + interaction.shutdown().unwrap(); +} diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/a.py b/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/a.py new file mode 100644 index 0000000000..ad8edf9681 --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/a.py @@ -0,0 +1,8 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + + +class UsersManager: + pass diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/b.py b/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/b.py new file mode 100644 index 0000000000..48879d4c4a --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/autoimport_common_prefix/b.py @@ -0,0 +1,8 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + + +class UsersController: + pass