rust.rs

   1use anyhow::{anyhow, Context as _, Result};
   2use async_compression::futures::bufread::GzipDecoder;
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::{io::BufReader, StreamExt};
   6use gpui::{App, AsyncApp, SharedString, Task};
   7use http_client::github::AssetKind;
   8use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
   9pub use language::*;
  10use lsp::LanguageServerBinary;
  11use regex::Regex;
  12use smol::fs::{self};
  13use std::fmt::Display;
  14use std::{
  15    any::Any,
  16    borrow::Cow,
  17    path::{Path, PathBuf},
  18    sync::{Arc, LazyLock},
  19};
  20use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  21use util::{fs::remove_matching, maybe, ResultExt};
  22
  23use crate::language_settings::language_settings;
  24
  25pub struct RustLspAdapter;
  26
  27#[cfg(target_os = "macos")]
  28impl RustLspAdapter {
  29    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  30    const ARCH_SERVER_NAME: &str = "apple-darwin";
  31}
  32
  33#[cfg(target_os = "linux")]
  34impl RustLspAdapter {
  35    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  36    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
  37}
  38
  39#[cfg(target_os = "freebsd")]
  40impl RustLspAdapter {
  41    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  42    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
  43}
  44
  45#[cfg(target_os = "windows")]
  46impl RustLspAdapter {
  47    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
  48    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
  49}
  50
  51impl RustLspAdapter {
  52    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
  53
  54    fn build_asset_name() -> String {
  55        let extension = match Self::GITHUB_ASSET_KIND {
  56            AssetKind::TarGz => "tar.gz",
  57            AssetKind::Gz => "gz",
  58            AssetKind::Zip => "zip",
  59        };
  60
  61        format!(
  62            "{}-{}-{}.{}",
  63            Self::SERVER_NAME,
  64            std::env::consts::ARCH,
  65            Self::ARCH_SERVER_NAME,
  66            extension
  67        )
  68    }
  69}
  70
  71pub(crate) struct CargoManifestProvider;
  72
  73impl ManifestProvider for CargoManifestProvider {
  74    fn name(&self) -> ManifestName {
  75        SharedString::new_static("Cargo.toml").into()
  76    }
  77
  78    fn search(
  79        &self,
  80        ManifestQuery {
  81            path,
  82            depth,
  83            delegate,
  84        }: ManifestQuery,
  85    ) -> Option<Arc<Path>> {
  86        let mut outermost_cargo_toml = None;
  87        for path in path.ancestors().take(depth) {
  88            let p = path.join("Cargo.toml");
  89            if delegate.exists(&p, Some(false)) {
  90                outermost_cargo_toml = Some(Arc::from(path));
  91            }
  92        }
  93
  94        outermost_cargo_toml
  95    }
  96}
  97
  98#[async_trait(?Send)]
  99impl LspAdapter for RustLspAdapter {
 100    fn name(&self) -> LanguageServerName {
 101        Self::SERVER_NAME.clone()
 102    }
 103
 104    fn manifest_name(&self) -> Option<ManifestName> {
 105        Some(SharedString::new_static("Cargo.toml").into())
 106    }
 107
 108    async fn check_if_user_installed(
 109        &self,
 110        delegate: &dyn LspAdapterDelegate,
 111        _: Arc<dyn LanguageToolchainStore>,
 112        _: &AsyncApp,
 113    ) -> Option<LanguageServerBinary> {
 114        let path = delegate.which("rust-analyzer".as_ref()).await?;
 115        let env = delegate.shell_env().await;
 116
 117        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
 118        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
 119        log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
 120        let result = delegate
 121            .try_exec(LanguageServerBinary {
 122                path: path.clone(),
 123                arguments: vec!["--help".into()],
 124                env: Some(env.clone()),
 125            })
 126            .await;
 127        if let Err(err) = result {
 128            log::error!(
 129                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
 130                path,
 131                err
 132            );
 133            return None;
 134        }
 135
 136        Some(LanguageServerBinary {
 137            path,
 138            env: Some(env),
 139            arguments: vec![],
 140        })
 141    }
 142
 143    async fn fetch_latest_server_version(
 144        &self,
 145        delegate: &dyn LspAdapterDelegate,
 146    ) -> Result<Box<dyn 'static + Send + Any>> {
 147        let release = latest_github_release(
 148            "rust-lang/rust-analyzer",
 149            true,
 150            false,
 151            delegate.http_client(),
 152        )
 153        .await?;
 154        let asset_name = Self::build_asset_name();
 155
 156        let asset = release
 157            .assets
 158            .iter()
 159            .find(|asset| asset.name == asset_name)
 160            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 161        Ok(Box::new(GitHubLspBinaryVersion {
 162            name: release.tag_name,
 163            url: asset.browser_download_url.clone(),
 164        }))
 165    }
 166
 167    async fn fetch_server_binary(
 168        &self,
 169        version: Box<dyn 'static + Send + Any>,
 170        container_dir: PathBuf,
 171        delegate: &dyn LspAdapterDelegate,
 172    ) -> Result<LanguageServerBinary> {
 173        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 174        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 175        let server_path = match Self::GITHUB_ASSET_KIND {
 176            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 177            AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
 178        };
 179
 180        if fs::metadata(&server_path).await.is_err() {
 181            remove_matching(&container_dir, |entry| entry != destination_path).await;
 182
 183            let mut response = delegate
 184                .http_client()
 185                .get(&version.url, Default::default(), true)
 186                .await
 187                .with_context(|| format!("downloading release from {}", version.url))?;
 188            match Self::GITHUB_ASSET_KIND {
 189                AssetKind::TarGz => {
 190                    let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
 191                    let archive = async_tar::Archive::new(decompressed_bytes);
 192                    archive.unpack(&destination_path).await.with_context(|| {
 193                        format!("extracting {} to {:?}", version.url, destination_path)
 194                    })?;
 195                }
 196                AssetKind::Gz => {
 197                    let mut decompressed_bytes =
 198                        GzipDecoder::new(BufReader::new(response.body_mut()));
 199                    let mut file =
 200                        fs::File::create(&destination_path).await.with_context(|| {
 201                            format!(
 202                                "creating a file {:?} for a download from {}",
 203                                destination_path, version.url,
 204                            )
 205                        })?;
 206                    futures::io::copy(&mut decompressed_bytes, &mut file)
 207                        .await
 208                        .with_context(|| {
 209                            format!("extracting {} to {:?}", version.url, destination_path)
 210                        })?;
 211                }
 212                AssetKind::Zip => {
 213                    node_runtime::extract_zip(
 214                        &destination_path,
 215                        BufReader::new(response.body_mut()),
 216                    )
 217                    .await
 218                    .with_context(|| {
 219                        format!("unzipping {} to {:?}", version.url, destination_path)
 220                    })?;
 221                }
 222            };
 223
 224            // todo("windows")
 225            #[cfg(not(windows))]
 226            {
 227                fs::set_permissions(
 228                    &server_path,
 229                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
 230                )
 231                .await?;
 232            }
 233        }
 234
 235        Ok(LanguageServerBinary {
 236            path: server_path,
 237            env: None,
 238            arguments: Default::default(),
 239        })
 240    }
 241
 242    async fn cached_server_binary(
 243        &self,
 244        container_dir: PathBuf,
 245        _: &dyn LspAdapterDelegate,
 246    ) -> Option<LanguageServerBinary> {
 247        get_cached_server_binary(container_dir).await
 248    }
 249
 250    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
 251        vec!["rustc".into()]
 252    }
 253
 254    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
 255        Some("rust-analyzer/flycheck".into())
 256    }
 257
 258    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
 259        static REGEX: LazyLock<Regex> =
 260            LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
 261
 262        for diagnostic in &mut params.diagnostics {
 263            for message in diagnostic
 264                .related_information
 265                .iter_mut()
 266                .flatten()
 267                .map(|info| &mut info.message)
 268                .chain([&mut diagnostic.message])
 269            {
 270                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
 271                    *message = sanitized;
 272                }
 273            }
 274        }
 275    }
 276
 277    async fn label_for_completion(
 278        &self,
 279        completion: &lsp::CompletionItem,
 280        language: &Arc<Language>,
 281    ) -> Option<CodeLabel> {
 282        let detail = completion
 283            .label_details
 284            .as_ref()
 285            .and_then(|detail| detail.detail.as_ref())
 286            .or(completion.detail.as_ref())
 287            .map(|detail| detail.trim());
 288        let function_signature = completion
 289            .label_details
 290            .as_ref()
 291            .and_then(|detail| detail.description.as_deref())
 292            .or(completion.detail.as_deref());
 293        match (detail, completion.kind) {
 294            (Some(detail), Some(lsp::CompletionItemKind::FIELD)) => {
 295                let name = &completion.label;
 296                let text = format!("{name}: {detail}");
 297                let prefix = "struct S { ";
 298                let source = Rope::from(format!("{prefix}{text} }}"));
 299                let runs =
 300                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 301                return Some(CodeLabel {
 302                    text,
 303                    runs,
 304                    filter_range: 0..name.len(),
 305                });
 306            }
 307            (
 308                Some(detail),
 309                Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
 310            ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
 311                let name = &completion.label;
 312                let text = format!(
 313                    "{}: {}",
 314                    name,
 315                    completion.detail.as_deref().unwrap_or(detail)
 316                );
 317                let prefix = "let ";
 318                let source = Rope::from(format!("{prefix}{text} = ();"));
 319                let runs =
 320                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 321                return Some(CodeLabel {
 322                    text,
 323                    runs,
 324                    filter_range: 0..name.len(),
 325                });
 326            }
 327            (
 328                Some(detail),
 329                Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
 330            ) => {
 331                static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
 332                const FUNCTION_PREFIXES: [&str; 6] = [
 333                    "async fn",
 334                    "async unsafe fn",
 335                    "const fn",
 336                    "const unsafe fn",
 337                    "unsafe fn",
 338                    "fn",
 339                ];
 340                // Is it function `async`?
 341                let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
 342                    function_signature.as_ref().and_then(|signature| {
 343                        signature
 344                            .strip_prefix(*prefix)
 345                            .map(|suffix| (*prefix, suffix))
 346                    })
 347                });
 348                // fn keyword should be followed by opening parenthesis.
 349                if let Some((prefix, suffix)) = fn_keyword {
 350                    let mut text = REGEX.replace(&completion.label, suffix).to_string();
 351                    let source = Rope::from(format!("{prefix} {text} {{}}"));
 352                    let run_start = prefix.len() + 1;
 353                    let runs = language.highlight_text(&source, run_start..run_start + text.len());
 354                    if detail.starts_with("(") {
 355                        text.push(' ');
 356                        text.push_str(&detail);
 357                    }
 358
 359                    return Some(CodeLabel {
 360                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
 361                        text,
 362                        runs,
 363                    });
 364                } else if completion
 365                    .detail
 366                    .as_ref()
 367                    .map_or(false, |detail| detail.starts_with("macro_rules! "))
 368                {
 369                    let source = Rope::from(completion.label.as_str());
 370                    let runs = language.highlight_text(&source, 0..completion.label.len());
 371
 372                    return Some(CodeLabel {
 373                        filter_range: 0..completion.label.len(),
 374                        text: completion.label.clone(),
 375                        runs,
 376                    });
 377                }
 378            }
 379            (_, Some(kind)) => {
 380                let highlight_name = match kind {
 381                    lsp::CompletionItemKind::STRUCT
 382                    | lsp::CompletionItemKind::INTERFACE
 383                    | lsp::CompletionItemKind::ENUM => Some("type"),
 384                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 385                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 386                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 387                        Some("constant")
 388                    }
 389                    _ => None,
 390                };
 391
 392                let mut label = completion.label.clone();
 393                if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) {
 394                    label.push(' ');
 395                    label.push_str(detail);
 396                }
 397                let mut label = CodeLabel::plain(label, None);
 398                if let Some(highlight_name) = highlight_name {
 399                    let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
 400                    label.runs.push((
 401                        0..label.text.rfind('(').unwrap_or(completion.label.len()),
 402                        highlight_id,
 403                    ));
 404                }
 405
 406                return Some(label);
 407            }
 408            _ => {}
 409        }
 410        None
 411    }
 412
 413    async fn label_for_symbol(
 414        &self,
 415        name: &str,
 416        kind: lsp::SymbolKind,
 417        language: &Arc<Language>,
 418    ) -> Option<CodeLabel> {
 419        let (text, filter_range, display_range) = match kind {
 420            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 421                let text = format!("fn {} () {{}}", name);
 422                let filter_range = 3..3 + name.len();
 423                let display_range = 0..filter_range.end;
 424                (text, filter_range, display_range)
 425            }
 426            lsp::SymbolKind::STRUCT => {
 427                let text = format!("struct {} {{}}", name);
 428                let filter_range = 7..7 + name.len();
 429                let display_range = 0..filter_range.end;
 430                (text, filter_range, display_range)
 431            }
 432            lsp::SymbolKind::ENUM => {
 433                let text = format!("enum {} {{}}", name);
 434                let filter_range = 5..5 + name.len();
 435                let display_range = 0..filter_range.end;
 436                (text, filter_range, display_range)
 437            }
 438            lsp::SymbolKind::INTERFACE => {
 439                let text = format!("trait {} {{}}", name);
 440                let filter_range = 6..6 + name.len();
 441                let display_range = 0..filter_range.end;
 442                (text, filter_range, display_range)
 443            }
 444            lsp::SymbolKind::CONSTANT => {
 445                let text = format!("const {}: () = ();", name);
 446                let filter_range = 6..6 + name.len();
 447                let display_range = 0..filter_range.end;
 448                (text, filter_range, display_range)
 449            }
 450            lsp::SymbolKind::MODULE => {
 451                let text = format!("mod {} {{}}", name);
 452                let filter_range = 4..4 + name.len();
 453                let display_range = 0..filter_range.end;
 454                (text, filter_range, display_range)
 455            }
 456            lsp::SymbolKind::TYPE_PARAMETER => {
 457                let text = format!("type {} {{}}", name);
 458                let filter_range = 5..5 + name.len();
 459                let display_range = 0..filter_range.end;
 460                (text, filter_range, display_range)
 461            }
 462            _ => return None,
 463        };
 464
 465        Some(CodeLabel {
 466            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
 467            text: text[display_range].to_string(),
 468            filter_range,
 469        })
 470    }
 471}
 472
 473pub(crate) struct RustContextProvider;
 474
 475const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 476    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 477
 478/// The bin name corresponding to the current file in Cargo.toml
 479const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 480    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 481
 482/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 483const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 484    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 485
 486const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 487    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 488
 489const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 490    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 491
 492const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 493    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 494
 495impl ContextProvider for RustContextProvider {
 496    fn build_context(
 497        &self,
 498        task_variables: &TaskVariables,
 499        location: &Location,
 500        project_env: Option<HashMap<String, String>>,
 501        _: Arc<dyn LanguageToolchainStore>,
 502        cx: &mut gpui::App,
 503    ) -> Task<Result<TaskVariables>> {
 504        let local_abs_path = location
 505            .buffer
 506            .read(cx)
 507            .file()
 508            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 509
 510        let local_abs_path = local_abs_path.as_deref();
 511
 512        let mut variables = TaskVariables::default();
 513
 514        if let Some(target) = local_abs_path
 515            .and_then(|path| package_name_and_bin_name_from_abs_path(path, project_env.as_ref()))
 516        {
 517            variables.extend(TaskVariables::from_iter([
 518                (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 519                (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 520                (
 521                    RUST_BIN_KIND_TASK_VARIABLE.clone(),
 522                    target.target_kind.to_string(),
 523                ),
 524            ]));
 525        }
 526
 527        if let Some(package_name) = local_abs_path
 528            .and_then(|local_abs_path| local_abs_path.parent())
 529            .and_then(|path| human_readable_package_name(path, project_env.as_ref()))
 530        {
 531            variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 532        }
 533
 534        if let (Some(path), Some(stem)) = (local_abs_path, task_variables.get(&VariableName::Stem))
 535        {
 536            let fragment = test_fragment(&variables, path, stem);
 537            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 538        };
 539        if let Some(test_name) =
 540            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 541        {
 542            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 543        }
 544        if let Some(doc_test_name) =
 545            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 546        {
 547            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 548        }
 549
 550        Task::ready(Ok(variables))
 551    }
 552
 553    fn associated_tasks(
 554        &self,
 555        file: Option<Arc<dyn language::File>>,
 556        cx: &App,
 557    ) -> Option<TaskTemplates> {
 558        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 559        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 560
 561        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 562        let package_to_run = language_sets
 563            .tasks
 564            .variables
 565            .get(DEFAULT_RUN_NAME_STR)
 566            .cloned();
 567        let custom_target_dir = language_sets
 568            .tasks
 569            .variables
 570            .get(CUSTOM_TARGET_DIR)
 571            .cloned();
 572        let run_task_args = if let Some(package_to_run) = package_to_run {
 573            vec!["run".into(), "-p".into(), package_to_run]
 574        } else {
 575            vec!["run".into()]
 576        };
 577        let mut task_templates = vec![
 578            TaskTemplate {
 579                label: format!(
 580                    "Check (package: {})",
 581                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 582                ),
 583                command: "cargo".into(),
 584                args: vec![
 585                    "check".into(),
 586                    "-p".into(),
 587                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 588                ],
 589                cwd: Some("$ZED_DIRNAME".to_owned()),
 590                ..TaskTemplate::default()
 591            },
 592            TaskTemplate {
 593                label: "Check all targets (workspace)".into(),
 594                command: "cargo".into(),
 595                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 596                cwd: Some("$ZED_DIRNAME".to_owned()),
 597                ..TaskTemplate::default()
 598            },
 599            TaskTemplate {
 600                label: format!(
 601                    "Test '{}' (package: {})",
 602                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 603                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 604                ),
 605                command: "cargo".into(),
 606                args: vec![
 607                    "test".into(),
 608                    "-p".into(),
 609                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 610                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 611                    "--".into(),
 612                    "--nocapture".into(),
 613                ],
 614                tags: vec!["rust-test".to_owned()],
 615                cwd: Some("$ZED_DIRNAME".to_owned()),
 616                ..TaskTemplate::default()
 617            },
 618            TaskTemplate {
 619                label: format!(
 620                    "Doc test '{}' (package: {})",
 621                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 622                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 623                ),
 624                command: "cargo".into(),
 625                args: vec![
 626                    "test".into(),
 627                    "--doc".into(),
 628                    "-p".into(),
 629                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 630                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 631                    "--".into(),
 632                    "--nocapture".into(),
 633                ],
 634                tags: vec!["rust-doc-test".to_owned()],
 635                cwd: Some("$ZED_DIRNAME".to_owned()),
 636                ..TaskTemplate::default()
 637            },
 638            TaskTemplate {
 639                label: format!(
 640                    "Test mod '{}' (package: {})",
 641                    VariableName::Stem.template_value(),
 642                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 643                ),
 644                command: "cargo".into(),
 645                args: vec![
 646                    "test".into(),
 647                    "-p".into(),
 648                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 649                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 650                ],
 651                tags: vec!["rust-mod-test".to_owned()],
 652                cwd: Some("$ZED_DIRNAME".to_owned()),
 653                ..TaskTemplate::default()
 654            },
 655            TaskTemplate {
 656                label: format!(
 657                    "Run {} {} (package: {})",
 658                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 659                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 660                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 661                ),
 662                command: "cargo".into(),
 663                args: vec![
 664                    "run".into(),
 665                    "-p".into(),
 666                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 667                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 668                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 669                ],
 670                cwd: Some("$ZED_DIRNAME".to_owned()),
 671                tags: vec!["rust-main".to_owned()],
 672                ..TaskTemplate::default()
 673            },
 674            TaskTemplate {
 675                label: format!(
 676                    "Test (package: {})",
 677                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 678                ),
 679                command: "cargo".into(),
 680                args: vec![
 681                    "test".into(),
 682                    "-p".into(),
 683                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 684                ],
 685                cwd: Some("$ZED_DIRNAME".to_owned()),
 686                ..TaskTemplate::default()
 687            },
 688            TaskTemplate {
 689                label: "Run".into(),
 690                command: "cargo".into(),
 691                args: run_task_args,
 692                cwd: Some("$ZED_DIRNAME".to_owned()),
 693                ..TaskTemplate::default()
 694            },
 695            TaskTemplate {
 696                label: "Clean".into(),
 697                command: "cargo".into(),
 698                args: vec!["clean".into()],
 699                cwd: Some("$ZED_DIRNAME".to_owned()),
 700                ..TaskTemplate::default()
 701            },
 702        ];
 703
 704        if let Some(custom_target_dir) = custom_target_dir {
 705            task_templates = task_templates
 706                .into_iter()
 707                .map(|mut task_template| {
 708                    let mut args = task_template.args.split_off(1);
 709                    task_template.args.append(&mut vec![
 710                        "--target-dir".to_string(),
 711                        custom_target_dir.clone(),
 712                    ]);
 713                    task_template.args.append(&mut args);
 714
 715                    task_template
 716                })
 717                .collect();
 718        }
 719
 720        Some(TaskTemplates(task_templates))
 721    }
 722}
 723
 724/// Part of the data structure of Cargo metadata
 725#[derive(serde::Deserialize)]
 726struct CargoMetadata {
 727    packages: Vec<CargoPackage>,
 728}
 729
 730#[derive(serde::Deserialize)]
 731struct CargoPackage {
 732    id: String,
 733    targets: Vec<CargoTarget>,
 734}
 735
 736#[derive(serde::Deserialize)]
 737struct CargoTarget {
 738    name: String,
 739    kind: Vec<String>,
 740    src_path: String,
 741}
 742
 743#[derive(Debug, PartialEq)]
 744enum TargetKind {
 745    Bin,
 746    Example,
 747}
 748
 749impl Display for TargetKind {
 750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 751        match self {
 752            TargetKind::Bin => write!(f, "bin"),
 753            TargetKind::Example => write!(f, "example"),
 754        }
 755    }
 756}
 757
 758impl TryFrom<&str> for TargetKind {
 759    type Error = ();
 760    fn try_from(value: &str) -> Result<Self, ()> {
 761        match value {
 762            "bin" => Ok(Self::Bin),
 763            "example" => Ok(Self::Example),
 764            _ => Err(()),
 765        }
 766    }
 767}
 768/// Which package and binary target are we in?
 769struct TargetInfo {
 770    package_name: String,
 771    target_name: String,
 772    target_kind: TargetKind,
 773}
 774
 775fn package_name_and_bin_name_from_abs_path(
 776    abs_path: &Path,
 777    project_env: Option<&HashMap<String, String>>,
 778) -> Option<TargetInfo> {
 779    let mut command = util::command::new_std_command("cargo");
 780    if let Some(envs) = project_env {
 781        command.envs(envs);
 782    }
 783    let output = command
 784        .current_dir(abs_path.parent()?)
 785        .arg("metadata")
 786        .arg("--no-deps")
 787        .arg("--format-version")
 788        .arg("1")
 789        .output()
 790        .log_err()?
 791        .stdout;
 792
 793    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
 794
 795    retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
 796        |(package_id, bin_name, target_kind)| {
 797            let package_name = package_name_from_pkgid(&package_id);
 798
 799            package_name.map(|package_name| TargetInfo {
 800                package_name: package_name.to_owned(),
 801                target_name: bin_name,
 802                target_kind,
 803            })
 804        },
 805    )
 806}
 807
 808fn retrieve_package_id_and_bin_name_from_metadata(
 809    metadata: CargoMetadata,
 810    abs_path: &Path,
 811) -> Option<(String, String, TargetKind)> {
 812    for package in metadata.packages {
 813        for target in package.targets {
 814            let Some(bin_kind) = target
 815                .kind
 816                .iter()
 817                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
 818            else {
 819                continue;
 820            };
 821            let target_path = PathBuf::from(target.src_path);
 822            if target_path == abs_path {
 823                return Some((package.id, target.name, bin_kind));
 824            }
 825        }
 826    }
 827
 828    None
 829}
 830
 831fn human_readable_package_name(
 832    package_directory: &Path,
 833    project_env: Option<&HashMap<String, String>>,
 834) -> Option<String> {
 835    let mut command = util::command::new_std_command("cargo");
 836    if let Some(envs) = project_env {
 837        command.envs(envs);
 838    }
 839    let pkgid = String::from_utf8(
 840        command
 841            .current_dir(package_directory)
 842            .arg("pkgid")
 843            .output()
 844            .log_err()?
 845            .stdout,
 846    )
 847    .ok()?;
 848    Some(package_name_from_pkgid(&pkgid)?.to_owned())
 849}
 850
 851// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
 852// Output example in the root of Zed project:
 853// ```sh
 854// ❯ cargo pkgid zed
 855// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
 856// ```
 857// Another variant, if a project has a custom package name or hyphen in the name:
 858// ```
 859// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
 860// ```
 861//
 862// Extracts the package name from the output according to the spec:
 863// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
 864fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
 865    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
 866        match input.rsplit_once(suffix_start) {
 867            Some((without_suffix, _)) => without_suffix,
 868            None => input,
 869        }
 870    }
 871
 872    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
 873    let package_name = match version_suffix.rsplit_once('@') {
 874        Some((custom_package_name, _version)) => custom_package_name,
 875        None => {
 876            let host_and_path = split_off_suffix(version_prefix, '?');
 877            let (_, package_name) = host_and_path.rsplit_once('/')?;
 878            package_name
 879        }
 880    };
 881    Some(package_name)
 882}
 883
 884async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
 885    maybe!(async {
 886        let mut last = None;
 887        let mut entries = fs::read_dir(&container_dir).await?;
 888        while let Some(entry) = entries.next().await {
 889            last = Some(entry?.path());
 890        }
 891
 892        anyhow::Ok(LanguageServerBinary {
 893            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
 894            env: None,
 895            arguments: Default::default(),
 896        })
 897    })
 898    .await
 899    .log_err()
 900}
 901
 902fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
 903    let fragment = if stem == "lib" {
 904        // This isn't quite right---it runs the tests for the entire library, rather than
 905        // just for the top-level `mod tests`. But we don't really have the means here to
 906        // filter out just that module.
 907        Some("--lib".to_owned())
 908    } else if stem == "mod" {
 909        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().to_string()) })
 910    } else if stem == "main" {
 911        if let (Some(bin_name), Some(bin_kind)) = (
 912            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
 913            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
 914        ) {
 915            Some(format!("--{bin_kind}={bin_name}"))
 916        } else {
 917            None
 918        }
 919    } else {
 920        Some(stem.to_owned())
 921    };
 922    fragment.unwrap_or_else(|| "--".to_owned())
 923}
 924
 925#[cfg(test)]
 926mod tests {
 927    use std::num::NonZeroU32;
 928
 929    use super::*;
 930    use crate::language;
 931    use gpui::{AppContext as _, BorrowAppContext, Hsla, TestAppContext};
 932    use language::language_settings::AllLanguageSettings;
 933    use lsp::CompletionItemLabelDetails;
 934    use settings::SettingsStore;
 935    use theme::SyntaxTheme;
 936    use util::path;
 937
 938    #[gpui::test]
 939    async fn test_process_rust_diagnostics() {
 940        let mut params = lsp::PublishDiagnosticsParams {
 941            uri: lsp::Url::from_file_path(path!("/a")).unwrap(),
 942            version: None,
 943            diagnostics: vec![
 944                // no newlines
 945                lsp::Diagnostic {
 946                    message: "use of moved value `a`".to_string(),
 947                    ..Default::default()
 948                },
 949                // newline at the end of a code span
 950                lsp::Diagnostic {
 951                    message: "consider importing this struct: `use b::c;\n`".to_string(),
 952                    ..Default::default()
 953                },
 954                // code span starting right after a newline
 955                lsp::Diagnostic {
 956                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 957                        .to_string(),
 958                    ..Default::default()
 959                },
 960            ],
 961        };
 962        RustLspAdapter.process_diagnostics(&mut params);
 963
 964        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 965
 966        // remove trailing newline from code span
 967        assert_eq!(
 968            params.diagnostics[1].message,
 969            "consider importing this struct: `use b::c;`"
 970        );
 971
 972        // do not remove newline before the start of code span
 973        assert_eq!(
 974            params.diagnostics[2].message,
 975            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 976        );
 977    }
 978
 979    #[gpui::test]
 980    async fn test_rust_label_for_completion() {
 981        let adapter = Arc::new(RustLspAdapter);
 982        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
 983        let grammar = language.grammar().unwrap();
 984        let theme = SyntaxTheme::new_test([
 985            ("type", Hsla::default()),
 986            ("keyword", Hsla::default()),
 987            ("function", Hsla::default()),
 988            ("property", Hsla::default()),
 989        ]);
 990
 991        language.set_theme(&theme);
 992
 993        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 994        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 995        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 996        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 997
 998        assert_eq!(
 999            adapter
1000                .label_for_completion(
1001                    &lsp::CompletionItem {
1002                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1003                        label: "hello(…)".to_string(),
1004                        label_details: Some(CompletionItemLabelDetails {
1005                            detail: Some("(use crate::foo)".into()),
1006                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1007                        }),
1008                        ..Default::default()
1009                    },
1010                    &language
1011                )
1012                .await,
1013            Some(CodeLabel {
1014                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1015                filter_range: 0..5,
1016                runs: vec![
1017                    (0..5, highlight_function),
1018                    (7..10, highlight_keyword),
1019                    (11..17, highlight_type),
1020                    (18..19, highlight_type),
1021                    (25..28, highlight_type),
1022                    (29..30, highlight_type),
1023                ],
1024            })
1025        );
1026        assert_eq!(
1027            adapter
1028                .label_for_completion(
1029                    &lsp::CompletionItem {
1030                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1031                        label: "hello(…)".to_string(),
1032                        label_details: Some(CompletionItemLabelDetails {
1033                            detail: Some(" (use crate::foo)".into()),
1034                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1035                        }),
1036                        ..Default::default()
1037                    },
1038                    &language
1039                )
1040                .await,
1041            Some(CodeLabel {
1042                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1043                filter_range: 0..5,
1044                runs: vec![
1045                    (0..5, highlight_function),
1046                    (7..10, highlight_keyword),
1047                    (11..17, highlight_type),
1048                    (18..19, highlight_type),
1049                    (25..28, highlight_type),
1050                    (29..30, highlight_type),
1051                ],
1052            })
1053        );
1054        assert_eq!(
1055            adapter
1056                .label_for_completion(
1057                    &lsp::CompletionItem {
1058                        kind: Some(lsp::CompletionItemKind::FIELD),
1059                        label: "len".to_string(),
1060                        detail: Some("usize".to_string()),
1061                        ..Default::default()
1062                    },
1063                    &language
1064                )
1065                .await,
1066            Some(CodeLabel {
1067                text: "len: usize".to_string(),
1068                filter_range: 0..3,
1069                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
1070            })
1071        );
1072
1073        assert_eq!(
1074            adapter
1075                .label_for_completion(
1076                    &lsp::CompletionItem {
1077                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1078                        label: "hello(…)".to_string(),
1079                        label_details: Some(CompletionItemLabelDetails {
1080                            detail: Some(" (use crate::foo)".to_string()),
1081                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1082                        }),
1083
1084                        ..Default::default()
1085                    },
1086                    &language
1087                )
1088                .await,
1089            Some(CodeLabel {
1090                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1091                filter_range: 0..5,
1092                runs: vec![
1093                    (0..5, highlight_function),
1094                    (7..10, highlight_keyword),
1095                    (11..17, highlight_type),
1096                    (18..19, highlight_type),
1097                    (25..28, highlight_type),
1098                    (29..30, highlight_type),
1099                ],
1100            })
1101        );
1102    }
1103
1104    #[gpui::test]
1105    async fn test_rust_label_for_symbol() {
1106        let adapter = Arc::new(RustLspAdapter);
1107        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1108        let grammar = language.grammar().unwrap();
1109        let theme = SyntaxTheme::new_test([
1110            ("type", Hsla::default()),
1111            ("keyword", Hsla::default()),
1112            ("function", Hsla::default()),
1113            ("property", Hsla::default()),
1114        ]);
1115
1116        language.set_theme(&theme);
1117
1118        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1119        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1120        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1121
1122        assert_eq!(
1123            adapter
1124                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1125                .await,
1126            Some(CodeLabel {
1127                text: "fn hello".to_string(),
1128                filter_range: 3..8,
1129                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1130            })
1131        );
1132
1133        assert_eq!(
1134            adapter
1135                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1136                .await,
1137            Some(CodeLabel {
1138                text: "type World".to_string(),
1139                filter_range: 5..10,
1140                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1141            })
1142        );
1143    }
1144
1145    #[gpui::test]
1146    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1147        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1148        cx.update(|cx| {
1149            let test_settings = SettingsStore::test(cx);
1150            cx.set_global(test_settings);
1151            language::init(cx);
1152            cx.update_global::<SettingsStore, _>(|store, cx| {
1153                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1154                    s.defaults.tab_size = NonZeroU32::new(2);
1155                });
1156            });
1157        });
1158
1159        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1160
1161        cx.new(|cx| {
1162            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1163
1164            // indent between braces
1165            buffer.set_text("fn a() {}", cx);
1166            let ix = buffer.len() - 1;
1167            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1168            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1169
1170            // indent between braces, even after empty lines
1171            buffer.set_text("fn a() {\n\n\n}", cx);
1172            let ix = buffer.len() - 2;
1173            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1174            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1175
1176            // indent a line that continues a field expression
1177            buffer.set_text("fn a() {\n  \n}", cx);
1178            let ix = buffer.len() - 2;
1179            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1180            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1181
1182            // indent further lines that continue the field expression, even after empty lines
1183            let ix = buffer.len() - 2;
1184            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1185            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1186
1187            // dedent the line after the field expression
1188            let ix = buffer.len() - 2;
1189            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1190            assert_eq!(
1191                buffer.text(),
1192                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1193            );
1194
1195            // indent inside a struct within a call
1196            buffer.set_text("const a: B = c(D {});", cx);
1197            let ix = buffer.len() - 3;
1198            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1199            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1200
1201            // indent further inside a nested call
1202            let ix = buffer.len() - 4;
1203            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1204            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1205
1206            // keep that indent after an empty line
1207            let ix = buffer.len() - 8;
1208            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1209            assert_eq!(
1210                buffer.text(),
1211                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1212            );
1213
1214            buffer
1215        });
1216    }
1217
1218    #[test]
1219    fn test_package_name_from_pkgid() {
1220        for (input, expected) in [
1221            (
1222                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1223                "zed",
1224            ),
1225            (
1226                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1227                "my-custom-package",
1228            ),
1229        ] {
1230            assert_eq!(package_name_from_pkgid(input), Some(expected));
1231        }
1232    }
1233
1234    #[test]
1235    fn test_retrieve_package_id_and_bin_name_from_metadata() {
1236        for (input, absolute_path, expected) in [
1237            (
1238                r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1239                "/path/to/zed/src/main.rs",
1240                Some((
1241                    "path+file:///path/to/zed/crates/zed#0.131.0",
1242                    "zed",
1243                    TargetKind::Bin,
1244                )),
1245            ),
1246            (
1247                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1248                "/path/to/custom-package/src/main.rs",
1249                Some((
1250                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1251                    "my-custom-bin",
1252                    TargetKind::Bin,
1253                )),
1254            ),
1255            (
1256                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1257                "/path/to/custom-package/src/main.rs",
1258                Some((
1259                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1260                    "my-custom-bin",
1261                    TargetKind::Example,
1262                )),
1263            ),
1264            (
1265                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-package","kind":["lib"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1266                "/path/to/custom-package/src/main.rs",
1267                None,
1268            ),
1269        ] {
1270            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1271
1272            let absolute_path = Path::new(absolute_path);
1273
1274            assert_eq!(
1275                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1276                expected.map(|(pkgid, name, kind)| (pkgid.to_owned(), name.to_owned(), kind))
1277            );
1278        }
1279    }
1280
1281    #[test]
1282    fn test_rust_test_fragment() {
1283        #[track_caller]
1284        fn check(
1285            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1286            path: &str,
1287            expected: &str,
1288        ) {
1289            let path = Path::new(path);
1290            let found = test_fragment(
1291                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1292                path,
1293                &path.file_stem().unwrap().to_str().unwrap(),
1294            );
1295            assert_eq!(expected, found);
1296        }
1297
1298        check([], "/project/src/lib.rs", "--lib");
1299        check([], "/project/src/foo/mod.rs", "foo");
1300        check(
1301            [
1302                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1303                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1304            ],
1305            "/project/src/main.rs",
1306            "--bin=x",
1307        );
1308        check([], "/project/src/main.rs", "--");
1309    }
1310}