editor: Fix package version completion partial accept and improve sorting (#43473)

Closes #41723

This PR fixes an issue with accepting partial semver completions by
including `.` in the completion query. This makes the editor treat the
entire version string as the query, instead of breaking segment at last
`.` .

This PR also adds a test for sorting semver completions. The actual
sorting fix is handled in the `package-version-server` by having it
provide `sort_text`. More:
https://github.com/zed-industries/package-version-server/pull/10

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/7657912f-c6da-4e05-956b-1c044918304f"
/>

Release Notes:

- Fixed an issue where accepting a completion for a semver version in
package.json would append the suggestion to the existing text instead of
replacing it.
- Improved the sorting of semver completions in package.json so the
latest versions appear at the top.
This commit is contained in:
Smit Barmase
2025-11-25 23:27:11 +05:30
committed by GitHub
parent 94f9b85859
commit 388fda2292
2 changed files with 117 additions and 8 deletions
+115 -7
View File
@@ -239,6 +239,89 @@ async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
assert_eq!(matches[2].string, "fetch_code_lens");
}
#[gpui::test]
async fn test_semver_label_sort_by_latest_version(cx: &mut TestAppContext) {
let mut versions = [
"10.4.112",
"10.4.22",
"10.4.2",
"10.4.20",
"10.4.21",
"10.4.12",
// Pre-release versions
"10.4.22-alpha",
"10.4.22-beta.1",
"10.4.22-rc.1",
// Build metadata versions
"10.4.21+build.123",
"10.4.20+20210327",
];
versions.sort_by(|a, b| {
match (
semver::Version::parse(a).ok(),
semver::Version::parse(b).ok(),
) {
(Some(a_ver), Some(b_ver)) => b_ver.cmp(&a_ver),
_ => std::cmp::Ordering::Equal,
}
});
let completions: Vec<_> = versions
.iter()
.enumerate()
.map(|(i, version)| {
// This sort text would come from the LSP
let sort_text = format!("{:08}", i);
CompletionBuilder::new(version, None, &sort_text, None)
})
.collect();
// Case 1: User types just the major and minor version
let matches =
filter_and_sort_matches("10.4.", &completions, SnippetSortOrder::default(), cx).await;
// Versions are ordered by recency (latest first)
let expected_versions = [
"10.4.112",
"10.4.22",
"10.4.22-rc.1",
"10.4.22-beta.1",
"10.4.22-alpha",
"10.4.21+build.123",
"10.4.21",
"10.4.20+20210327",
"10.4.20",
"10.4.12",
"10.4.2",
];
for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
assert_eq!(match_item.string.as_ref() as &str, *expected);
}
// Case 2: User types the major, minor, and patch version
let matches =
filter_and_sort_matches("10.4.2", &completions, SnippetSortOrder::default(), cx).await;
let expected_versions = [
// Exact match comes first
"10.4.2",
// Ordered by recency with exact major, minor, and patch versions
"10.4.22",
"10.4.22-rc.1",
"10.4.22-beta.1",
"10.4.22-alpha",
"10.4.21+build.123",
"10.4.21",
"10.4.20+20210327",
"10.4.20",
// Versions with non-exact patch versions are ordered by fuzzy score
// Higher fuzzy score than 112 patch version since "2" appears before "1"
// in "12", making it rank higher than "112"
"10.4.12",
"10.4.112",
];
for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
assert_eq!(match_item.string.as_ref() as &str, *expected);
}
}
async fn test_for_each_prefix<F>(
target: &str,
completions: &Vec<Completion>,
@@ -259,30 +342,55 @@ struct CompletionBuilder;
impl CompletionBuilder {
fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
Self::new(label, filter_text, sort_text, CompletionItemKind::CONSTANT)
Self::new(
label,
filter_text,
sort_text,
Some(CompletionItemKind::CONSTANT),
)
}
fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
Self::new(label, filter_text, sort_text, CompletionItemKind::FUNCTION)
Self::new(
label,
filter_text,
sort_text,
Some(CompletionItemKind::FUNCTION),
)
}
fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
Self::new(label, filter_text, sort_text, CompletionItemKind::METHOD)
Self::new(
label,
filter_text,
sort_text,
Some(CompletionItemKind::METHOD),
)
}
fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
Self::new(label, filter_text, sort_text, CompletionItemKind::VARIABLE)
Self::new(
label,
filter_text,
sort_text,
Some(CompletionItemKind::VARIABLE),
)
}
fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
Self::new(label, filter_text, sort_text, CompletionItemKind::SNIPPET)
Self::new(
label,
filter_text,
sort_text,
Some(CompletionItemKind::SNIPPET),
)
}
fn new(
label: &str,
filter_text: Option<&str>,
sort_text: &str,
kind: CompletionItemKind,
kind: Option<CompletionItemKind>,
) -> Completion {
Completion {
replace_range: Anchor::MIN..Anchor::MAX,
@@ -294,7 +402,7 @@ impl CompletionBuilder {
server_id: LanguageServerId(0),
lsp_completion: Box::new(CompletionItem {
label: label.to_string(),
kind: Some(kind),
kind: kind,
sort_text: Some(sort_text.to_string()),
filter_text: filter_text.map(|text| text.to_string()),
..Default::default()
+2 -1
View File
@@ -11,5 +11,6 @@ brackets = [
tab_size = 2
prettier_parser_name = "json"
debuggers = ["JavaScript"]
[overrides.string]
completion_query_characters = [":", " "]
completion_query_characters = [":", " ", "."]