From 2e7607c0e73778e53a506c57d60850f1641b95a0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:34:57 +0200 Subject: [PATCH] python: Fix instability of Pyright/BasedPyright code completions (#37850) Pyright sets different `sortText` based on whether a given completion item was recently resolved. This probably lines up with VSCode's way of resolving items, but it's a no-no for us, as it makes completions unstable. Closes #9983 Release Notes: - python: Fixed code completions having arbitrary order when using Pyright/basedpyright --- crates/languages/src/python.rs | 54 +++++++++------------------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 978f22d91c26c604ba670d712589199a64275950..659a6b017c9eec85be4d950f2cac18d9ccdc7f17 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -87,6 +87,18 @@ fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } +/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. +/// Where `XX` is the sorting category, `YYYY` is based on most recent usage, +/// and `name` is the symbol name itself. +/// +/// The problem with it is that Pyright adjusts the sort text based on previous resolutions (items for which we've issued `completion/resolve` call have their sortText adjusted), +/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail. +/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 +fn process_pyright_completions(items: &mut [lsp::CompletionItem]) { + for item in items { + item.sort_text.take(); + } +} pub struct PythonLspAdapter { node: NodeRuntime, } @@ -232,26 +244,7 @@ impl LspAdapter for PythonLspAdapter { } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { - // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. - // Where `XX` is the sorting category, `YYYY` is based on most recent usage, - // and `name` is the symbol name itself. - // - // Because the symbol name is included, there generally are not ties when - // sorting by the `sortText`, so the symbol's fuzzy match score is not taken - // into account. Here, we remove the symbol name from the sortText in order - // to allow our own fuzzy score to be used to break ties. - // - // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - for item in items { - let Some(sort_text) = &mut item.sort_text else { - continue; - }; - let mut parts = sort_text.split('.'); - let Some(first) = parts.next() else { continue }; - let Some(second) = parts.next() else { continue }; - let Some(_) = parts.next() else { continue }; - sort_text.replace_range(first.len() + second.len() + 1.., ""); - } + process_pyright_completions(items); } async fn label_for_completion( @@ -1490,26 +1483,7 @@ impl LspAdapter for BasedPyrightLspAdapter { } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { - // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. - // Where `XX` is the sorting category, `YYYY` is based on most recent usage, - // and `name` is the symbol name itself. - // - // Because the symbol name is included, there generally are not ties when - // sorting by the `sortText`, so the symbol's fuzzy match score is not taken - // into account. Here, we remove the symbol name from the sortText in order - // to allow our own fuzzy score to be used to break ties. - // - // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - for item in items { - let Some(sort_text) = &mut item.sort_text else { - continue; - }; - let mut parts = sort_text.split('.'); - let Some(first) = parts.next() else { continue }; - let Some(second) = parts.next() else { continue }; - let Some(_) = parts.next() else { continue }; - sort_text.replace_range(first.len() + second.len() + 1.., ""); - } + process_pyright_completions(items); } async fn label_for_completion(