Properly extract package name out of cargo pkgid (#10929)

Kirill Bulatov created

Fixes https://github.com/zed-industries/zed/issues/10925

Uses correct package name to generate Rust `cargo` tasks.
Also deduplicates lines in task modal item tooltips.

Release Notes:

- Fixed Rust tasks using incorrect package name
([10925](https://github.com/zed-industries/zed/issues/10925))

Change summary

crates/languages/src/rust.rs | 70 +++++++++++++++++++++++++++----------
crates/tasks_ui/src/modal.rs |  4 +
2 files changed, 53 insertions(+), 21 deletions(-)

Detailed changes

crates/languages/src/rust.rs 🔗

@@ -421,13 +421,6 @@ impl ContextProvider for RustContextProvider {
 }
 
 fn human_readable_package_name(package_directory: &Path) -> Option<String> {
-    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
-        match input.rsplit_once(suffix_start) {
-            Some((without_suffix, _)) => without_suffix,
-            None => input,
-        }
-    }
-
     let pkgid = String::from_utf8(
         std::process::Command::new("cargo")
             .current_dir(package_directory)
@@ -437,19 +430,40 @@ fn human_readable_package_name(package_directory: &Path) -> Option<String> {
             .stdout,
     )
     .ok()?;
-    // For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
-    // Output example in the root of Zed project:
-    // ```bash
-    // ❯ cargo pkgid zed
-    // path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
-    // ```
-    // Extrarct the package name from the output according to the spec:
-    // https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
-    let mut package_name = pkgid.trim();
-    package_name = split_off_suffix(package_name, '#');
-    package_name = split_off_suffix(package_name, '?');
-    let (_, package_name) = package_name.rsplit_once('/')?;
-    Some(package_name.to_string())
+    Some(package_name_from_pkgid(&pkgid)?.to_owned())
+}
+
+// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
+// Output example in the root of Zed project:
+// ```bash
+// ❯ cargo pkgid zed
+// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
+// ```
+// Another variant, if a project has a custom package name or hyphen in the name:
+// ```
+// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
+// ```
+//
+// Extracts the package name from the output according to the spec:
+// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
+fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
+    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
+        match input.rsplit_once(suffix_start) {
+            Some((without_suffix, _)) => without_suffix,
+            None => input,
+        }
+    }
+
+    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
+    let package_name = match version_suffix.rsplit_once('@') {
+        Some((custom_package_name, _version)) => custom_package_name,
+        None => {
+            let host_and_path = split_off_suffix(version_prefix, '?');
+            let (_, package_name) = host_and_path.rsplit_once('/')?;
+            package_name
+        }
+    };
+    Some(package_name)
 }
 
 async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
@@ -750,4 +764,20 @@ mod tests {
             buffer
         });
     }
+
+    #[test]
+    fn test_package_name_from_pkgid() {
+        for (input, expected) in [
+            (
+                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
+                "zed",
+            ),
+            (
+                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
+                "my-custom-package",
+            ),
+        ] {
+            assert_eq!(package_name_from_pkgid(input), Some(expected));
+        }
+    }
 }

crates/tasks_ui/src/modal.rs 🔗

@@ -313,7 +313,9 @@ impl PickerDelegate for TasksModalDelegate {
             String::new()
         };
         if let Some(resolved) = resolved_task.resolved.as_ref() {
-            if display_label != resolved.command_label {
+            if resolved.command_label != display_label
+                && resolved.command_label != resolved_task.resolved_label
+            {
                 if !tooltip_label_text.trim().is_empty() {
                     tooltip_label_text.push('\n');
                 }