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
This commit is contained in:
Piotr Osiewicz
2025-09-09 15:34:57 +00:00
committed by GitHub
parent 0ac1752668
commit 2e7607c0e7
+14 -40
View File
@@ -87,6 +87,18 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
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(