rust.rs

   1use anyhow::{Context as _, Result};
   2use async_trait::async_trait;
   3use collections::HashMap;
   4use futures::StreamExt;
   5use gpui::{App, AppContext, AsyncApp, SharedString, Task};
   6use http_client::github::AssetKind;
   7use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
   8use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
   9pub use language::*;
  10use lsp::{InitializeParams, LanguageServerBinary};
  11use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
  12use project::project_settings::ProjectSettings;
  13use regex::Regex;
  14use serde_json::json;
  15use settings::Settings as _;
  16use smallvec::SmallVec;
  17use smol::fs::{self};
  18use std::cmp::Reverse;
  19use std::fmt::Display;
  20use std::ops::Range;
  21use std::{
  22    borrow::Cow,
  23    path::{Path, PathBuf},
  24    sync::{Arc, LazyLock},
  25};
  26use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  27use util::fs::{make_file_executable, remove_matching};
  28use util::merge_json_value_into;
  29use util::rel_path::RelPath;
  30use util::{ResultExt, maybe};
  31
  32use crate::language_settings::language_settings;
  33
  34pub struct RustLspAdapter;
  35
  36#[cfg(target_os = "macos")]
  37impl RustLspAdapter {
  38    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  39    const ARCH_SERVER_NAME: &str = "apple-darwin";
  40}
  41
  42#[cfg(target_os = "linux")]
  43impl RustLspAdapter {
  44    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  45    const ARCH_SERVER_NAME: &str = "unknown-linux";
  46}
  47
  48#[cfg(target_os = "freebsd")]
  49impl RustLspAdapter {
  50    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  51    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
  52}
  53
  54#[cfg(target_os = "windows")]
  55impl RustLspAdapter {
  56    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
  57    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
  58}
  59
  60const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
  61
  62#[cfg(target_os = "linux")]
  63enum LibcType {
  64    Gnu,
  65    Musl,
  66}
  67
  68impl RustLspAdapter {
  69    #[cfg(target_os = "linux")]
  70    async fn determine_libc_type() -> LibcType {
  71        use futures::pin_mut;
  72
  73        async fn from_ldd_version() -> Option<LibcType> {
  74            use util::command::new_smol_command;
  75
  76            let ldd_output = new_smol_command("ldd")
  77                .arg("--version")
  78                .output()
  79                .await
  80                .ok()?;
  81            let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
  82
  83            if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
  84                Some(LibcType::Gnu)
  85            } else if ldd_version.contains("musl") {
  86                Some(LibcType::Musl)
  87            } else {
  88                None
  89            }
  90        }
  91
  92        if let Some(libc_type) = from_ldd_version().await {
  93            return libc_type;
  94        }
  95
  96        let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
  97            // defaulting to gnu because nix doesn't have /lib files due to not following FHS
  98            return LibcType::Gnu;
  99        };
 100        let dir_entries = dir_entries.filter_map(async move |e| e.ok());
 101        pin_mut!(dir_entries);
 102
 103        let mut has_musl = false;
 104        let mut has_gnu = false;
 105
 106        while let Some(entry) = dir_entries.next().await {
 107            let file_name = entry.file_name();
 108            let file_name = file_name.to_string_lossy();
 109            if file_name.starts_with("ld-musl-") {
 110                has_musl = true;
 111            } else if file_name.starts_with("ld-linux-") {
 112                has_gnu = true;
 113            }
 114        }
 115
 116        match (has_musl, has_gnu) {
 117            (true, _) => LibcType::Musl,
 118            (_, true) => LibcType::Gnu,
 119            _ => LibcType::Gnu,
 120        }
 121    }
 122
 123    #[cfg(target_os = "linux")]
 124    async fn build_arch_server_name_linux() -> String {
 125        let libc = match Self::determine_libc_type().await {
 126            LibcType::Musl => "musl",
 127            LibcType::Gnu => "gnu",
 128        };
 129
 130        format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
 131    }
 132
 133    async fn build_asset_name() -> String {
 134        let extension = match Self::GITHUB_ASSET_KIND {
 135            AssetKind::TarGz => "tar.gz",
 136            AssetKind::Gz => "gz",
 137            AssetKind::Zip => "zip",
 138        };
 139
 140        #[cfg(target_os = "linux")]
 141        let arch_server_name = Self::build_arch_server_name_linux().await;
 142        #[cfg(not(target_os = "linux"))]
 143        let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
 144
 145        format!(
 146            "{}-{}-{}.{}",
 147            SERVER_NAME,
 148            std::env::consts::ARCH,
 149            &arch_server_name,
 150            extension
 151        )
 152    }
 153}
 154
 155pub(crate) struct CargoManifestProvider;
 156
 157impl ManifestProvider for CargoManifestProvider {
 158    fn name(&self) -> ManifestName {
 159        SharedString::new_static("Cargo.toml").into()
 160    }
 161
 162    fn search(
 163        &self,
 164        ManifestQuery {
 165            path,
 166            depth,
 167            delegate,
 168        }: ManifestQuery,
 169    ) -> Option<Arc<RelPath>> {
 170        let mut outermost_cargo_toml = None;
 171        for path in path.ancestors().take(depth) {
 172            let p = path.join(RelPath::unix("Cargo.toml").unwrap());
 173            if delegate.exists(&p, Some(false)) {
 174                outermost_cargo_toml = Some(Arc::from(path));
 175            }
 176        }
 177
 178        outermost_cargo_toml
 179    }
 180}
 181
 182#[async_trait(?Send)]
 183impl LspAdapter for RustLspAdapter {
 184    fn name(&self) -> LanguageServerName {
 185        SERVER_NAME
 186    }
 187
 188    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
 189        vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
 190    }
 191
 192    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
 193        Some("rust-analyzer/flycheck".into())
 194    }
 195
 196    fn process_diagnostics(
 197        &self,
 198        params: &mut lsp::PublishDiagnosticsParams,
 199        _: LanguageServerId,
 200        _: Option<&'_ Buffer>,
 201    ) {
 202        static REGEX: LazyLock<Regex> =
 203            LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
 204
 205        for diagnostic in &mut params.diagnostics {
 206            for message in diagnostic
 207                .related_information
 208                .iter_mut()
 209                .flatten()
 210                .map(|info| &mut info.message)
 211                .chain([&mut diagnostic.message])
 212            {
 213                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
 214                    *message = sanitized;
 215                }
 216            }
 217        }
 218    }
 219
 220    fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
 221        static REGEX: LazyLock<Regex> =
 222            LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));
 223        Some(REGEX.replace_all(message, "\n\n").to_string())
 224    }
 225
 226    async fn label_for_completion(
 227        &self,
 228        completion: &lsp::CompletionItem,
 229        language: &Arc<Language>,
 230    ) -> Option<CodeLabel> {
 231        // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered
 232        // this usually contains signatures of the thing to be completed
 233        let detail_right = completion
 234            .label_details
 235            .as_ref()
 236            .and_then(|detail| detail.description.as_ref())
 237            .or(completion.detail.as_ref())
 238            .map(|detail| detail.trim());
 239        // this tends to contain alias and import information
 240        let mut detail_left = completion
 241            .label_details
 242            .as_ref()
 243            .and_then(|detail| detail.detail.as_deref());
 244        let mk_label = |text: String, filter_range: &dyn Fn() -> Range<usize>, runs| {
 245            let filter_range = completion
 246                .filter_text
 247                .as_deref()
 248                .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
 249                .or_else(|| {
 250                    text.find(&completion.label)
 251                        .map(|ix| ix..ix + completion.label.len())
 252                })
 253                .unwrap_or_else(filter_range);
 254
 255            CodeLabel::new(text, filter_range, runs)
 256        };
 257        let mut label = match (detail_right, completion.kind) {
 258            (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
 259                let name = &completion.label;
 260                let text = format!("{name}: {signature}");
 261                let prefix = "struct S { ";
 262                let source = Rope::from_iter([prefix, &text, " }"]);
 263                let runs =
 264                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 265                mk_label(text, &|| 0..completion.label.len(), runs)
 266            }
 267            (
 268                Some(signature),
 269                Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
 270            ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
 271                let name = &completion.label;
 272                let text = format!("{name}: {signature}",);
 273                let prefix = "let ";
 274                let source = Rope::from_iter([prefix, &text, " = ();"]);
 275                let runs =
 276                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 277                mk_label(text, &|| 0..completion.label.len(), runs)
 278            }
 279            (
 280                function_signature,
 281                Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
 282            ) => {
 283                const FUNCTION_PREFIXES: [&str; 6] = [
 284                    "async fn",
 285                    "async unsafe fn",
 286                    "const fn",
 287                    "const unsafe fn",
 288                    "unsafe fn",
 289                    "fn",
 290                ];
 291                let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {
 292                    function_signature?
 293                        .strip_prefix(prefix)
 294                        .map(|suffix| (prefix, suffix))
 295                });
 296                let label = if let Some(label) = completion
 297                    .label
 298                    .strip_suffix("(…)")
 299                    .or_else(|| completion.label.strip_suffix("()"))
 300                {
 301                    label
 302                } else {
 303                    &completion.label
 304                };
 305
 306                static FULL_SIGNATURE_REGEX: LazyLock<Regex> =
 307                    LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX"));
 308                if let Some((function_signature, match_)) = function_signature
 309                    .filter(|it| it.contains(&label))
 310                    .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))
 311                {
 312                    let source = Rope::from(function_signature);
 313                    let runs = language.highlight_text(&source, 0..function_signature.len());
 314                    mk_label(
 315                        function_signature.to_owned(),
 316                        &|| match_.range().start - 3..match_.range().end - 1,
 317                        runs,
 318                    )
 319                } else if let Some((prefix, suffix)) = fn_prefixed {
 320                    let text = format!("{label}{suffix}");
 321                    let source = Rope::from_iter([prefix, " ", &text, " {}"]);
 322                    let run_start = prefix.len() + 1;
 323                    let runs = language.highlight_text(&source, run_start..run_start + text.len());
 324                    mk_label(text, &|| 0..label.len(), runs)
 325                } else if completion
 326                    .detail
 327                    .as_ref()
 328                    .is_some_and(|detail| detail.starts_with("macro_rules! "))
 329                {
 330                    let text = completion.label.clone();
 331                    let len = text.len();
 332                    let source = Rope::from(text.as_str());
 333                    let runs = language.highlight_text(&source, 0..len);
 334                    mk_label(text, &|| 0..completion.label.len(), runs)
 335                } else if detail_left.is_none() {
 336                    return None;
 337                } else {
 338                    mk_label(
 339                        completion.label.clone(),
 340                        &|| 0..completion.label.len(),
 341                        vec![],
 342                    )
 343                }
 344            }
 345            (_, kind) => {
 346                let mut label;
 347                let mut runs = vec![];
 348
 349                if completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
 350                    && let Some(
 351                        lsp::CompletionTextEdit::InsertAndReplace(lsp::InsertReplaceEdit {
 352                            new_text,
 353                            ..
 354                        })
 355                        | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
 356                    ) = completion.text_edit.as_ref()
 357                    && let Ok(mut snippet) = snippet::Snippet::parse(new_text)
 358                    && snippet.tabstops.len() > 1
 359                {
 360                    label = String::new();
 361
 362                    // we never display the final tabstop
 363                    snippet.tabstops.remove(snippet.tabstops.len() - 1);
 364
 365                    let mut text_pos = 0;
 366
 367                    let mut all_stop_ranges = snippet
 368                        .tabstops
 369                        .into_iter()
 370                        .flat_map(|stop| stop.ranges)
 371                        .collect::<SmallVec<[_; 8]>>();
 372                    all_stop_ranges.sort_unstable_by_key(|a| (a.start, Reverse(a.end)));
 373
 374                    for range in &all_stop_ranges {
 375                        let start_pos = range.start as usize;
 376                        let end_pos = range.end as usize;
 377
 378                        label.push_str(&snippet.text[text_pos..start_pos]);
 379
 380                        if start_pos == end_pos {
 381                            let caret_start = label.len();
 382                            label.push('…');
 383                            runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
 384                        } else {
 385                            let label_start = label.len();
 386                            label.push_str(&snippet.text[start_pos..end_pos]);
 387                            let label_end = label.len();
 388                            runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
 389                        }
 390
 391                        text_pos = end_pos;
 392                    }
 393
 394                    label.push_str(&snippet.text[text_pos..]);
 395
 396                    if detail_left.is_some_and(|detail_left| detail_left == new_text) {
 397                        // We only include the left detail if it isn't the snippet again
 398                        detail_left.take();
 399                    }
 400
 401                    runs.extend(language.highlight_text(&Rope::from(&label), 0..label.len()));
 402                } else {
 403                    let highlight_name = kind.and_then(|kind| match kind {
 404                        lsp::CompletionItemKind::STRUCT
 405                        | lsp::CompletionItemKind::INTERFACE
 406                        | lsp::CompletionItemKind::ENUM => Some("type"),
 407                        lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 408                        lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 409                        lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 410                            Some("constant")
 411                        }
 412                        _ => None,
 413                    });
 414
 415                    label = completion.label.clone();
 416
 417                    if let Some(highlight_name) = highlight_name {
 418                        let highlight_id =
 419                            language.grammar()?.highlight_id_for_name(highlight_name)?;
 420                        runs.push((
 421                            0..label.rfind('(').unwrap_or(completion.label.len()),
 422                            highlight_id,
 423                        ));
 424                    } else if detail_left.is_none()
 425                        && kind != Some(lsp::CompletionItemKind::SNIPPET)
 426                    {
 427                        return None;
 428                    }
 429                }
 430
 431                let label_len = label.len();
 432
 433                mk_label(label, &|| 0..label_len, runs)
 434            }
 435        };
 436
 437        if let Some(detail_left) = detail_left {
 438            label.text.push(' ');
 439            if !detail_left.starts_with('(') {
 440                label.text.push('(');
 441            }
 442            label.text.push_str(detail_left);
 443            if !detail_left.ends_with(')') {
 444                label.text.push(')');
 445            }
 446        }
 447
 448        Some(label)
 449    }
 450
 451    async fn label_for_symbol(
 452        &self,
 453        name: &str,
 454        kind: lsp::SymbolKind,
 455        language: &Arc<Language>,
 456    ) -> Option<CodeLabel> {
 457        let (prefix, suffix) = match kind {
 458            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"),
 459            lsp::SymbolKind::STRUCT => ("struct ", " {}"),
 460            lsp::SymbolKind::ENUM => ("enum ", " {}"),
 461            lsp::SymbolKind::INTERFACE => ("trait ", " {}"),
 462            lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"),
 463            lsp::SymbolKind::MODULE => ("mod ", " {}"),
 464            lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"),
 465            _ => return None,
 466        };
 467
 468        let filter_range = prefix.len()..prefix.len() + name.len();
 469        let display_range = 0..filter_range.end;
 470        Some(CodeLabel::new(
 471            format!("{prefix}{name}"),
 472            filter_range,
 473            language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
 474        ))
 475    }
 476
 477    fn prepare_initialize_params(
 478        &self,
 479        mut original: InitializeParams,
 480        cx: &App,
 481    ) -> Result<InitializeParams> {
 482        let enable_lsp_tasks = ProjectSettings::get_global(cx)
 483            .lsp
 484            .get(&SERVER_NAME)
 485            .is_some_and(|s| s.enable_lsp_tasks);
 486        if enable_lsp_tasks {
 487            let experimental = json!({
 488                "runnables": {
 489                    "kinds": [ "cargo", "shell" ],
 490                },
 491            });
 492            if let Some(original_experimental) = &mut original.capabilities.experimental {
 493                merge_json_value_into(experimental, original_experimental);
 494            } else {
 495                original.capabilities.experimental = Some(experimental);
 496            }
 497        }
 498
 499        Ok(original)
 500    }
 501}
 502
 503impl LspInstaller for RustLspAdapter {
 504    type BinaryVersion = GitHubLspBinaryVersion;
 505    async fn check_if_user_installed(
 506        &self,
 507        delegate: &dyn LspAdapterDelegate,
 508        _: Option<Toolchain>,
 509        _: &AsyncApp,
 510    ) -> Option<LanguageServerBinary> {
 511        let path = delegate.which("rust-analyzer".as_ref()).await?;
 512        let env = delegate.shell_env().await;
 513
 514        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
 515        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
 516        log::debug!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
 517        let result = delegate
 518            .try_exec(LanguageServerBinary {
 519                path: path.clone(),
 520                arguments: vec!["--help".into()],
 521                env: Some(env.clone()),
 522            })
 523            .await;
 524        if let Err(err) = result {
 525            log::debug!(
 526                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
 527                path,
 528                err
 529            );
 530            return None;
 531        }
 532
 533        Some(LanguageServerBinary {
 534            path,
 535            env: Some(env),
 536            arguments: vec![],
 537        })
 538    }
 539
 540    async fn fetch_latest_server_version(
 541        &self,
 542        delegate: &dyn LspAdapterDelegate,
 543        pre_release: bool,
 544        _: &mut AsyncApp,
 545    ) -> Result<GitHubLspBinaryVersion> {
 546        let release = latest_github_release(
 547            "rust-lang/rust-analyzer",
 548            true,
 549            pre_release,
 550            delegate.http_client(),
 551        )
 552        .await?;
 553        let asset_name = Self::build_asset_name().await;
 554        let asset = release
 555            .assets
 556            .into_iter()
 557            .find(|asset| asset.name == asset_name)
 558            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 559        Ok(GitHubLspBinaryVersion {
 560            name: release.tag_name,
 561            url: asset.browser_download_url,
 562            digest: asset.digest,
 563        })
 564    }
 565
 566    async fn fetch_server_binary(
 567        &self,
 568        version: GitHubLspBinaryVersion,
 569        container_dir: PathBuf,
 570        delegate: &dyn LspAdapterDelegate,
 571    ) -> Result<LanguageServerBinary> {
 572        let GitHubLspBinaryVersion {
 573            name,
 574            url,
 575            digest: expected_digest,
 576        } = version;
 577        let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
 578        let server_path = match Self::GITHUB_ASSET_KIND {
 579            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 580            AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
 581        };
 582
 583        let binary = LanguageServerBinary {
 584            path: server_path.clone(),
 585            env: None,
 586            arguments: Default::default(),
 587        };
 588
 589        let metadata_path = destination_path.with_extension("metadata");
 590        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
 591            .await
 592            .ok();
 593        if let Some(metadata) = metadata {
 594            let validity_check = async || {
 595                delegate
 596                    .try_exec(LanguageServerBinary {
 597                        path: server_path.clone(),
 598                        arguments: vec!["--version".into()],
 599                        env: None,
 600                    })
 601                    .await
 602                    .inspect_err(|err| {
 603                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
 604                    })
 605            };
 606            if let (Some(actual_digest), Some(expected_digest)) =
 607                (&metadata.digest, &expected_digest)
 608            {
 609                if actual_digest == expected_digest {
 610                    if validity_check().await.is_ok() {
 611                        return Ok(binary);
 612                    }
 613                } else {
 614                    log::info!(
 615                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
 616                    );
 617                }
 618            } else if validity_check().await.is_ok() {
 619                return Ok(binary);
 620            }
 621        }
 622
 623        download_server_binary(
 624            &*delegate.http_client(),
 625            &url,
 626            expected_digest.as_deref(),
 627            &destination_path,
 628            Self::GITHUB_ASSET_KIND,
 629        )
 630        .await?;
 631        make_file_executable(&server_path).await?;
 632        remove_matching(&container_dir, |path| path != destination_path).await;
 633        GithubBinaryMetadata::write_to_file(
 634            &GithubBinaryMetadata {
 635                metadata_version: 1,
 636                digest: expected_digest,
 637            },
 638            &metadata_path,
 639        )
 640        .await?;
 641
 642        Ok(LanguageServerBinary {
 643            path: server_path,
 644            env: None,
 645            arguments: Default::default(),
 646        })
 647    }
 648
 649    async fn cached_server_binary(
 650        &self,
 651        container_dir: PathBuf,
 652        _: &dyn LspAdapterDelegate,
 653    ) -> Option<LanguageServerBinary> {
 654        get_cached_server_binary(container_dir).await
 655    }
 656}
 657
 658pub(crate) struct RustContextProvider;
 659
 660const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 661    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 662
 663/// The bin name corresponding to the current file in Cargo.toml
 664const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 665    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 666
 667/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 668const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 669    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 670
 671/// The flag to list required features for executing a bin, if any
 672const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
 673    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
 674
 675/// The list of required features for executing a bin, if any
 676const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
 677    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
 678
 679const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 680    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 681
 682const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 683    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 684
 685const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 686    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 687
 688const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
 689    VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
 690
 691impl ContextProvider for RustContextProvider {
 692    fn build_context(
 693        &self,
 694        task_variables: &TaskVariables,
 695        location: ContextLocation<'_>,
 696        project_env: Option<HashMap<String, String>>,
 697        _: Arc<dyn LanguageToolchainStore>,
 698        cx: &mut gpui::App,
 699    ) -> Task<Result<TaskVariables>> {
 700        let local_abs_path = location
 701            .file_location
 702            .buffer
 703            .read(cx)
 704            .file()
 705            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 706
 707        let mut variables = TaskVariables::default();
 708
 709        if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
 710        {
 711            let fragment = test_fragment(&variables, path, stem);
 712            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 713        };
 714        if let Some(test_name) =
 715            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 716        {
 717            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 718        }
 719        if let Some(doc_test_name) =
 720            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 721        {
 722            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 723        }
 724        cx.background_spawn(async move {
 725            if let Some(path) = local_abs_path
 726                .as_deref()
 727                .and_then(|local_abs_path| local_abs_path.parent())
 728                && let Some(package_name) =
 729                    human_readable_package_name(path, project_env.as_ref()).await
 730            {
 731                variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 732            }
 733            if let Some(path) = local_abs_path.as_ref()
 734                && let Some((target, manifest_path)) =
 735                    target_info_from_abs_path(path, project_env.as_ref()).await
 736            {
 737                if let Some(target) = target {
 738                    variables.extend(TaskVariables::from_iter([
 739                        (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 740                        (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 741                        (
 742                            RUST_BIN_KIND_TASK_VARIABLE.clone(),
 743                            target.target_kind.to_string(),
 744                        ),
 745                    ]));
 746                    if target.required_features.is_empty() {
 747                        variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
 748                        variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
 749                    } else {
 750                        variables.insert(
 751                            RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
 752                            "--features".to_string(),
 753                        );
 754                        variables.insert(
 755                            RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
 756                            target.required_features.join(","),
 757                        );
 758                    }
 759                }
 760                variables.extend(TaskVariables::from_iter([(
 761                    RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
 762                    manifest_path.to_string_lossy().into_owned(),
 763                )]));
 764            }
 765            Ok(variables)
 766        })
 767    }
 768
 769    fn associated_tasks(
 770        &self,
 771        file: Option<Arc<dyn language::File>>,
 772        cx: &App,
 773    ) -> Task<Option<TaskTemplates>> {
 774        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 775        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 776
 777        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 778        let package_to_run = language_sets
 779            .tasks
 780            .variables
 781            .get(DEFAULT_RUN_NAME_STR)
 782            .cloned();
 783        let custom_target_dir = language_sets
 784            .tasks
 785            .variables
 786            .get(CUSTOM_TARGET_DIR)
 787            .cloned();
 788        let run_task_args = if let Some(package_to_run) = package_to_run {
 789            vec!["run".into(), "-p".into(), package_to_run]
 790        } else {
 791            vec!["run".into()]
 792        };
 793        let mut task_templates = vec![
 794            TaskTemplate {
 795                label: format!(
 796                    "Check (package: {})",
 797                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 798                ),
 799                command: "cargo".into(),
 800                args: vec![
 801                    "check".into(),
 802                    "-p".into(),
 803                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 804                ],
 805                cwd: Some("$ZED_DIRNAME".to_owned()),
 806                ..TaskTemplate::default()
 807            },
 808            TaskTemplate {
 809                label: "Check all targets (workspace)".into(),
 810                command: "cargo".into(),
 811                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 812                cwd: Some("$ZED_DIRNAME".to_owned()),
 813                ..TaskTemplate::default()
 814            },
 815            TaskTemplate {
 816                label: format!(
 817                    "Test '{}' (package: {})",
 818                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 819                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 820                ),
 821                command: "cargo".into(),
 822                args: vec![
 823                    "test".into(),
 824                    "-p".into(),
 825                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 826                    "--".into(),
 827                    "--nocapture".into(),
 828                    "--include-ignored".into(),
 829                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 830                ],
 831                tags: vec!["rust-test".to_owned()],
 832                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 833                ..TaskTemplate::default()
 834            },
 835            TaskTemplate {
 836                label: format!(
 837                    "Doc test '{}' (package: {})",
 838                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 839                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 840                ),
 841                command: "cargo".into(),
 842                args: vec![
 843                    "test".into(),
 844                    "--doc".into(),
 845                    "-p".into(),
 846                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 847                    "--".into(),
 848                    "--nocapture".into(),
 849                    "--include-ignored".into(),
 850                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 851                ],
 852                tags: vec!["rust-doc-test".to_owned()],
 853                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 854                ..TaskTemplate::default()
 855            },
 856            TaskTemplate {
 857                label: format!(
 858                    "Test mod '{}' (package: {})",
 859                    VariableName::Stem.template_value(),
 860                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 861                ),
 862                command: "cargo".into(),
 863                args: vec![
 864                    "test".into(),
 865                    "-p".into(),
 866                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 867                    "--".into(),
 868                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 869                ],
 870                tags: vec!["rust-mod-test".to_owned()],
 871                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 872                ..TaskTemplate::default()
 873            },
 874            TaskTemplate {
 875                label: format!(
 876                    "Run {} {} (package: {})",
 877                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 878                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 879                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 880                ),
 881                command: "cargo".into(),
 882                args: vec![
 883                    "run".into(),
 884                    "-p".into(),
 885                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 886                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 887                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 888                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
 889                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
 890                ],
 891                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 892                tags: vec!["rust-main".to_owned()],
 893                ..TaskTemplate::default()
 894            },
 895            TaskTemplate {
 896                label: format!(
 897                    "Test (package: {})",
 898                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 899                ),
 900                command: "cargo".into(),
 901                args: vec![
 902                    "test".into(),
 903                    "-p".into(),
 904                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 905                ],
 906                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 907                ..TaskTemplate::default()
 908            },
 909            TaskTemplate {
 910                label: "Run".into(),
 911                command: "cargo".into(),
 912                args: run_task_args,
 913                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 914                ..TaskTemplate::default()
 915            },
 916            TaskTemplate {
 917                label: "Clean".into(),
 918                command: "cargo".into(),
 919                args: vec!["clean".into()],
 920                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 921                ..TaskTemplate::default()
 922            },
 923        ];
 924
 925        if let Some(custom_target_dir) = custom_target_dir {
 926            task_templates = task_templates
 927                .into_iter()
 928                .map(|mut task_template| {
 929                    let mut args = task_template.args.split_off(1);
 930                    task_template.args.append(&mut vec![
 931                        "--target-dir".to_string(),
 932                        custom_target_dir.clone(),
 933                    ]);
 934                    task_template.args.append(&mut args);
 935
 936                    task_template
 937                })
 938                .collect();
 939        }
 940
 941        Task::ready(Some(TaskTemplates(task_templates)))
 942    }
 943
 944    fn lsp_task_source(&self) -> Option<LanguageServerName> {
 945        Some(SERVER_NAME)
 946    }
 947}
 948
 949/// Part of the data structure of Cargo metadata
 950#[derive(Debug, serde::Deserialize)]
 951struct CargoMetadata {
 952    packages: Vec<CargoPackage>,
 953}
 954
 955#[derive(Debug, serde::Deserialize)]
 956struct CargoPackage {
 957    id: String,
 958    targets: Vec<CargoTarget>,
 959    manifest_path: Arc<Path>,
 960}
 961
 962#[derive(Debug, serde::Deserialize)]
 963struct CargoTarget {
 964    name: String,
 965    kind: Vec<String>,
 966    src_path: String,
 967    #[serde(rename = "required-features", default)]
 968    required_features: Vec<String>,
 969}
 970
 971#[derive(Debug, PartialEq)]
 972enum TargetKind {
 973    Bin,
 974    Example,
 975}
 976
 977impl Display for TargetKind {
 978    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 979        match self {
 980            TargetKind::Bin => write!(f, "bin"),
 981            TargetKind::Example => write!(f, "example"),
 982        }
 983    }
 984}
 985
 986impl TryFrom<&str> for TargetKind {
 987    type Error = ();
 988    fn try_from(value: &str) -> Result<Self, ()> {
 989        match value {
 990            "bin" => Ok(Self::Bin),
 991            "example" => Ok(Self::Example),
 992            _ => Err(()),
 993        }
 994    }
 995}
 996/// Which package and binary target are we in?
 997#[derive(Debug, PartialEq)]
 998struct TargetInfo {
 999    package_name: String,
1000    target_name: String,
1001    target_kind: TargetKind,
1002    required_features: Vec<String>,
1003}
1004
1005async fn target_info_from_abs_path(
1006    abs_path: &Path,
1007    project_env: Option<&HashMap<String, String>>,
1008) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1009    let mut command = util::command::new_smol_command("cargo");
1010    if let Some(envs) = project_env {
1011        command.envs(envs);
1012    }
1013    let output = command
1014        .current_dir(abs_path.parent()?)
1015        .arg("metadata")
1016        .arg("--no-deps")
1017        .arg("--format-version")
1018        .arg("1")
1019        .output()
1020        .await
1021        .log_err()?
1022        .stdout;
1023
1024    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1025    target_info_from_metadata(metadata, abs_path)
1026}
1027
1028fn target_info_from_metadata(
1029    metadata: CargoMetadata,
1030    abs_path: &Path,
1031) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1032    let mut manifest_path = None;
1033    for package in metadata.packages {
1034        let Some(manifest_dir_path) = package.manifest_path.parent() else {
1035            continue;
1036        };
1037
1038        let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1039            continue;
1040        };
1041        let candidate_path_length = path_from_manifest_dir.components().count();
1042        // Pick the most specific manifest path
1043        if let Some((path, current_length)) = &mut manifest_path {
1044            if candidate_path_length > *current_length {
1045                *path = Arc::from(manifest_dir_path);
1046                *current_length = candidate_path_length;
1047            }
1048        } else {
1049            manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1050        };
1051
1052        for target in package.targets {
1053            let Some(bin_kind) = target
1054                .kind
1055                .iter()
1056                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1057            else {
1058                continue;
1059            };
1060            let target_path = PathBuf::from(target.src_path);
1061            if target_path == abs_path {
1062                return manifest_path.map(|(path, _)| {
1063                    (
1064                        package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1065                            package_name: package_name.to_owned(),
1066                            target_name: target.name,
1067                            required_features: target.required_features,
1068                            target_kind: bin_kind,
1069                        }),
1070                        path,
1071                    )
1072                });
1073            }
1074        }
1075    }
1076
1077    manifest_path.map(|(path, _)| (None, path))
1078}
1079
1080async fn human_readable_package_name(
1081    package_directory: &Path,
1082    project_env: Option<&HashMap<String, String>>,
1083) -> Option<String> {
1084    let mut command = util::command::new_smol_command("cargo");
1085    if let Some(envs) = project_env {
1086        command.envs(envs);
1087    }
1088    let pkgid = String::from_utf8(
1089        command
1090            .current_dir(package_directory)
1091            .arg("pkgid")
1092            .output()
1093            .await
1094            .log_err()?
1095            .stdout,
1096    )
1097    .ok()?;
1098    Some(package_name_from_pkgid(&pkgid)?.to_owned())
1099}
1100
1101// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1102// Output example in the root of Zed project:
1103// ```sh
1104// ❯ cargo pkgid zed
1105// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1106// ```
1107// Another variant, if a project has a custom package name or hyphen in the name:
1108// ```
1109// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1110// ```
1111//
1112// Extracts the package name from the output according to the spec:
1113// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1114fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1115    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1116        match input.rsplit_once(suffix_start) {
1117            Some((without_suffix, _)) => without_suffix,
1118            None => input,
1119        }
1120    }
1121
1122    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1123    let package_name = match version_suffix.rsplit_once('@') {
1124        Some((custom_package_name, _version)) => custom_package_name,
1125        None => {
1126            let host_and_path = split_off_suffix(version_prefix, '?');
1127            let (_, package_name) = host_and_path.rsplit_once('/')?;
1128            package_name
1129        }
1130    };
1131    Some(package_name)
1132}
1133
1134async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1135    let binary_result = maybe!(async {
1136        let mut last = None;
1137        let mut entries = fs::read_dir(&container_dir)
1138            .await
1139            .with_context(|| format!("listing {container_dir:?}"))?;
1140        while let Some(entry) = entries.next().await {
1141            let path = entry?.path();
1142            if path.extension().is_some_and(|ext| ext == "metadata") {
1143                continue;
1144            }
1145            last = Some(path);
1146        }
1147
1148        let path = match last {
1149            Some(last) => last,
1150            None => return Ok(None),
1151        };
1152        let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1153            AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1154            AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1155        };
1156
1157        anyhow::Ok(Some(LanguageServerBinary {
1158            path,
1159            env: None,
1160            arguments: Vec::new(),
1161        }))
1162    })
1163    .await;
1164
1165    match binary_result {
1166        Ok(Some(binary)) => Some(binary),
1167        Ok(None) => {
1168            log::info!("No cached rust-analyzer binary found");
1169            None
1170        }
1171        Err(e) => {
1172            log::error!("Failed to look up cached rust-analyzer binary: {e:#}");
1173            None
1174        }
1175    }
1176}
1177
1178fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1179    let fragment = if stem == "lib" {
1180        // This isn't quite right---it runs the tests for the entire library, rather than
1181        // just for the top-level `mod tests`. But we don't really have the means here to
1182        // filter out just that module.
1183        Some("--lib".to_owned())
1184    } else if stem == "mod" {
1185        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1186    } else if stem == "main" {
1187        if let (Some(bin_name), Some(bin_kind)) = (
1188            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1189            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1190        ) {
1191            Some(format!("--{bin_kind}={bin_name}"))
1192        } else {
1193            None
1194        }
1195    } else {
1196        Some(stem.to_owned())
1197    };
1198    fragment.unwrap_or_else(|| "--".to_owned())
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use std::num::NonZeroU32;
1204
1205    use super::*;
1206    use crate::language;
1207    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1208    use lsp::CompletionItemLabelDetails;
1209    use settings::SettingsStore;
1210    use theme::SyntaxTheme;
1211    use util::path;
1212
1213    #[gpui::test]
1214    async fn test_process_rust_diagnostics() {
1215        let mut params = lsp::PublishDiagnosticsParams {
1216            uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1217            version: None,
1218            diagnostics: vec![
1219                // no newlines
1220                lsp::Diagnostic {
1221                    message: "use of moved value `a`".to_string(),
1222                    ..Default::default()
1223                },
1224                // newline at the end of a code span
1225                lsp::Diagnostic {
1226                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1227                    ..Default::default()
1228                },
1229                // code span starting right after a newline
1230                lsp::Diagnostic {
1231                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1232                        .to_string(),
1233                    ..Default::default()
1234                },
1235            ],
1236        };
1237        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1238
1239        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1240
1241        // remove trailing newline from code span
1242        assert_eq!(
1243            params.diagnostics[1].message,
1244            "consider importing this struct: `use b::c;`"
1245        );
1246
1247        // do not remove newline before the start of code span
1248        assert_eq!(
1249            params.diagnostics[2].message,
1250            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1251        );
1252    }
1253
1254    #[gpui::test]
1255    async fn test_rust_label_for_completion() {
1256        let adapter = Arc::new(RustLspAdapter);
1257        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1258        let grammar = language.grammar().unwrap();
1259        let theme = SyntaxTheme::new_test([
1260            ("type", Hsla::default()),
1261            ("keyword", Hsla::default()),
1262            ("function", Hsla::default()),
1263            ("property", Hsla::default()),
1264        ]);
1265
1266        language.set_theme(&theme);
1267
1268        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1269        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1270        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1271        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1272
1273        assert_eq!(
1274            adapter
1275                .label_for_completion(
1276                    &lsp::CompletionItem {
1277                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1278                        label: "hello(…)".to_string(),
1279                        label_details: Some(CompletionItemLabelDetails {
1280                            detail: Some("(use crate::foo)".into()),
1281                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1282                        }),
1283                        ..Default::default()
1284                    },
1285                    &language
1286                )
1287                .await,
1288            Some(CodeLabel::new(
1289                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1290                0..5,
1291                vec![
1292                    (0..5, highlight_function),
1293                    (7..10, highlight_keyword),
1294                    (11..17, highlight_type),
1295                    (18..19, highlight_type),
1296                    (25..28, highlight_type),
1297                    (29..30, highlight_type),
1298                ],
1299            ))
1300        );
1301        assert_eq!(
1302            adapter
1303                .label_for_completion(
1304                    &lsp::CompletionItem {
1305                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1306                        label: "hello(…)".to_string(),
1307                        label_details: Some(CompletionItemLabelDetails {
1308                            detail: Some("(use crate::foo)".into()),
1309                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1310                        }),
1311                        ..Default::default()
1312                    },
1313                    &language
1314                )
1315                .await,
1316            Some(CodeLabel::new(
1317                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1318                0..5,
1319                vec![
1320                    (0..5, highlight_function),
1321                    (7..10, highlight_keyword),
1322                    (11..17, highlight_type),
1323                    (18..19, highlight_type),
1324                    (25..28, highlight_type),
1325                    (29..30, highlight_type),
1326                ],
1327            ))
1328        );
1329        assert_eq!(
1330            adapter
1331                .label_for_completion(
1332                    &lsp::CompletionItem {
1333                        kind: Some(lsp::CompletionItemKind::FIELD),
1334                        label: "len".to_string(),
1335                        detail: Some("usize".to_string()),
1336                        ..Default::default()
1337                    },
1338                    &language
1339                )
1340                .await,
1341            Some(CodeLabel::new(
1342                "len: usize".to_string(),
1343                0..3,
1344                vec![(0..3, highlight_field), (5..10, highlight_type),],
1345            ))
1346        );
1347
1348        assert_eq!(
1349            adapter
1350                .label_for_completion(
1351                    &lsp::CompletionItem {
1352                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1353                        label: "hello(…)".to_string(),
1354                        label_details: Some(CompletionItemLabelDetails {
1355                            detail: Some("(use crate::foo)".to_string()),
1356                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1357                        }),
1358
1359                        ..Default::default()
1360                    },
1361                    &language
1362                )
1363                .await,
1364            Some(CodeLabel::new(
1365                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1366                0..5,
1367                vec![
1368                    (0..5, highlight_function),
1369                    (7..10, highlight_keyword),
1370                    (11..17, highlight_type),
1371                    (18..19, highlight_type),
1372                    (25..28, highlight_type),
1373                    (29..30, highlight_type),
1374                ],
1375            ))
1376        );
1377
1378        assert_eq!(
1379            adapter
1380                .label_for_completion(
1381                    &lsp::CompletionItem {
1382                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1383                        label: "hello".to_string(),
1384                        label_details: Some(CompletionItemLabelDetails {
1385                            detail: Some("(use crate::foo)".to_string()),
1386                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1387                        }),
1388                        ..Default::default()
1389                    },
1390                    &language
1391                )
1392                .await,
1393            Some(CodeLabel::new(
1394                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1395                0..5,
1396                vec![
1397                    (0..5, highlight_function),
1398                    (7..10, highlight_keyword),
1399                    (11..17, highlight_type),
1400                    (18..19, highlight_type),
1401                    (25..28, highlight_type),
1402                    (29..30, highlight_type),
1403                ],
1404            ))
1405        );
1406
1407        assert_eq!(
1408            adapter
1409                .label_for_completion(
1410                    &lsp::CompletionItem {
1411                        kind: Some(lsp::CompletionItemKind::METHOD),
1412                        label: "await.as_deref_mut()".to_string(),
1413                        filter_text: Some("as_deref_mut".to_string()),
1414                        label_details: Some(CompletionItemLabelDetails {
1415                            detail: None,
1416                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1417                        }),
1418                        ..Default::default()
1419                    },
1420                    &language
1421                )
1422                .await,
1423            Some(CodeLabel::new(
1424                "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1425                6..18,
1426                vec![
1427                    (6..18, HighlightId(2)),
1428                    (20..23, HighlightId(1)),
1429                    (33..40, HighlightId(0)),
1430                    (45..46, HighlightId(0))
1431                ],
1432            ))
1433        );
1434
1435        assert_eq!(
1436            adapter
1437                .label_for_completion(
1438                    &lsp::CompletionItem {
1439                        kind: Some(lsp::CompletionItemKind::METHOD),
1440                        label: "as_deref_mut()".to_string(),
1441                        filter_text: Some("as_deref_mut".to_string()),
1442                        label_details: Some(CompletionItemLabelDetails {
1443                            detail: None,
1444                            description: Some(
1445                                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1446                            ),
1447                        }),
1448                        ..Default::default()
1449                    },
1450                    &language
1451                )
1452                .await,
1453            Some(CodeLabel::new(
1454                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1455                7..19,
1456                vec![
1457                    (0..3, HighlightId(1)),
1458                    (4..6, HighlightId(1)),
1459                    (7..19, HighlightId(2)),
1460                    (21..24, HighlightId(1)),
1461                    (34..41, HighlightId(0)),
1462                    (46..47, HighlightId(0))
1463                ],
1464            ))
1465        );
1466
1467        assert_eq!(
1468            adapter
1469                .label_for_completion(
1470                    &lsp::CompletionItem {
1471                        kind: Some(lsp::CompletionItemKind::FIELD),
1472                        label: "inner_value".to_string(),
1473                        filter_text: Some("value".to_string()),
1474                        detail: Some("String".to_string()),
1475                        ..Default::default()
1476                    },
1477                    &language,
1478                )
1479                .await,
1480            Some(CodeLabel::new(
1481                "inner_value: String".to_string(),
1482                6..11,
1483                vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1484            ))
1485        );
1486
1487        // Snippet with insert tabstop (empty placeholder)
1488        assert_eq!(
1489            adapter
1490                .label_for_completion(
1491                    &lsp::CompletionItem {
1492                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1493                        label: "println!".to_string(),
1494                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1495                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1496                            range: lsp::Range::default(),
1497                            new_text: "println!(\"$1\", $2)$0".to_string(),
1498                        })),
1499                        ..Default::default()
1500                    },
1501                    &language,
1502                )
1503                .await,
1504            Some(CodeLabel::new(
1505                "println!(\"\", …)".to_string(),
1506                0..8,
1507                vec![
1508                    (10..13, HighlightId::TABSTOP_INSERT_ID),
1509                    (16..19, HighlightId::TABSTOP_INSERT_ID),
1510                    (0..7, HighlightId(2)),
1511                    (7..8, HighlightId(2)),
1512                ],
1513            ))
1514        );
1515
1516        // Snippet with replace tabstop (placeholder with default text)
1517        assert_eq!(
1518            adapter
1519                .label_for_completion(
1520                    &lsp::CompletionItem {
1521                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1522                        label: "vec!".to_string(),
1523                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1524                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1525                            range: lsp::Range::default(),
1526                            new_text: "vec![${1:elem}]$0".to_string(),
1527                        })),
1528                        ..Default::default()
1529                    },
1530                    &language,
1531                )
1532                .await,
1533            Some(CodeLabel::new(
1534                "vec![elem]".to_string(),
1535                0..4,
1536                vec![
1537                    (5..9, HighlightId::TABSTOP_REPLACE_ID),
1538                    (0..3, HighlightId(2)),
1539                    (3..4, HighlightId(2)),
1540                ],
1541            ))
1542        );
1543
1544        // Snippet with tabstop appearing more than once
1545        assert_eq!(
1546            adapter
1547                .label_for_completion(
1548                    &lsp::CompletionItem {
1549                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1550                        label: "if let".to_string(),
1551                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1552                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1553                            range: lsp::Range::default(),
1554                            new_text: "if let ${1:pat} = $1 {\n    $0\n}".to_string(),
1555                        })),
1556                        ..Default::default()
1557                    },
1558                    &language,
1559                )
1560                .await,
1561            Some(CodeLabel::new(
1562                "if let pat = … {\n    \n}".to_string(),
1563                0..6,
1564                vec![
1565                    (7..10, HighlightId::TABSTOP_REPLACE_ID),
1566                    (13..16, HighlightId::TABSTOP_INSERT_ID),
1567                    (0..2, HighlightId(1)),
1568                    (3..6, HighlightId(1)),
1569                ],
1570            ))
1571        );
1572
1573        // Snippet with tabstops not in left-to-right order
1574        assert_eq!(
1575            adapter
1576                .label_for_completion(
1577                    &lsp::CompletionItem {
1578                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1579                        label: "for".to_string(),
1580                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1581                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1582                            range: lsp::Range::default(),
1583                            new_text: "for ${2:item} in ${1:iter} {\n    $0\n}".to_string(),
1584                        })),
1585                        ..Default::default()
1586                    },
1587                    &language,
1588                )
1589                .await,
1590            Some(CodeLabel::new(
1591                "for item in iter {\n    \n}".to_string(),
1592                0..3,
1593                vec![
1594                    (4..8, HighlightId::TABSTOP_REPLACE_ID),
1595                    (12..16, HighlightId::TABSTOP_REPLACE_ID),
1596                    (0..3, HighlightId(1)),
1597                    (9..11, HighlightId(1)),
1598                ],
1599            ))
1600        );
1601
1602        // Postfix completion without actual tabstops (only implicit final $0)
1603        // The label should use completion.label so it can be filtered by "ref"
1604        let ref_completion = adapter
1605            .label_for_completion(
1606                &lsp::CompletionItem {
1607                    kind: Some(lsp::CompletionItemKind::SNIPPET),
1608                    label: "ref".to_string(),
1609                    filter_text: Some("ref".to_string()),
1610                    label_details: Some(CompletionItemLabelDetails {
1611                        detail: None,
1612                        description: Some("&expr".to_string()),
1613                    }),
1614                    detail: Some("&expr".to_string()),
1615                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1616                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1617                        range: lsp::Range::default(),
1618                        new_text: "&String::new()".to_string(),
1619                    })),
1620                    ..Default::default()
1621                },
1622                &language,
1623            )
1624            .await;
1625        assert!(
1626            ref_completion.is_some(),
1627            "ref postfix completion should have a label"
1628        );
1629        let ref_label = ref_completion.unwrap();
1630        let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1631        assert!(
1632            filter_text.contains("ref"),
1633            "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1634        );
1635
1636        // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1637        let res = adapter
1638            .label_for_completion(
1639                &lsp::CompletionItem {
1640                    kind: Some(lsp::CompletionItemKind::STRUCT),
1641                    label: "Particles".to_string(),
1642                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1643                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1644                        range: lsp::Range::default(),
1645                        new_text: "Particles { pos_x: $1, pos_y: $2, vel_x: $3, vel_y: $4, acc_x: ${5:()}, acc_y: ${6:()}, mass: $7 }$0".to_string(),
1646                    })),
1647                    ..Default::default()
1648                },
1649                &language,
1650            )
1651            .await
1652            .unwrap();
1653
1654        assert_eq!(
1655            res,
1656            CodeLabel::new(
1657                "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1658                0..9,
1659                vec![
1660                    (19..22, HighlightId::TABSTOP_INSERT_ID),
1661                    (31..34, HighlightId::TABSTOP_INSERT_ID),
1662                    (43..46, HighlightId::TABSTOP_INSERT_ID),
1663                    (55..58, HighlightId::TABSTOP_INSERT_ID),
1664                    (67..69, HighlightId::TABSTOP_REPLACE_ID),
1665                    (78..80, HighlightId::TABSTOP_REPLACE_ID),
1666                    (88..91, HighlightId::TABSTOP_INSERT_ID),
1667                    (0..9, highlight_type),
1668                    (60..65, highlight_field),
1669                    (71..76, highlight_field),
1670                ],
1671            )
1672        );
1673    }
1674
1675    #[gpui::test]
1676    async fn test_rust_label_for_symbol() {
1677        let adapter = Arc::new(RustLspAdapter);
1678        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1679        let grammar = language.grammar().unwrap();
1680        let theme = SyntaxTheme::new_test([
1681            ("type", Hsla::default()),
1682            ("keyword", Hsla::default()),
1683            ("function", Hsla::default()),
1684            ("property", Hsla::default()),
1685        ]);
1686
1687        language.set_theme(&theme);
1688
1689        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1690        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1691        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1692
1693        assert_eq!(
1694            adapter
1695                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1696                .await,
1697            Some(CodeLabel::new(
1698                "fn hello".to_string(),
1699                3..8,
1700                vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1701            ))
1702        );
1703
1704        assert_eq!(
1705            adapter
1706                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1707                .await,
1708            Some(CodeLabel::new(
1709                "type World".to_string(),
1710                5..10,
1711                vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1712            ))
1713        );
1714    }
1715
1716    #[gpui::test]
1717    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1718        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1719        cx.update(|cx| {
1720            let test_settings = SettingsStore::test(cx);
1721            cx.set_global(test_settings);
1722            cx.update_global::<SettingsStore, _>(|store, cx| {
1723                store.update_user_settings(cx, |s| {
1724                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1725                });
1726            });
1727        });
1728
1729        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1730
1731        cx.new(|cx| {
1732            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1733
1734            // indent between braces
1735            buffer.set_text("fn a() {}", cx);
1736            let ix = buffer.len() - 1;
1737            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1738            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1739
1740            // indent between braces, even after empty lines
1741            buffer.set_text("fn a() {\n\n\n}", cx);
1742            let ix = buffer.len() - 2;
1743            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1744            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1745
1746            // indent a line that continues a field expression
1747            buffer.set_text("fn a() {\n  \n}", cx);
1748            let ix = buffer.len() - 2;
1749            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1750            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1751
1752            // indent further lines that continue the field expression, even after empty lines
1753            let ix = buffer.len() - 2;
1754            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1755            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1756
1757            // dedent the line after the field expression
1758            let ix = buffer.len() - 2;
1759            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1760            assert_eq!(
1761                buffer.text(),
1762                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1763            );
1764
1765            // indent inside a struct within a call
1766            buffer.set_text("const a: B = c(D {});", cx);
1767            let ix = buffer.len() - 3;
1768            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1769            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1770
1771            // indent further inside a nested call
1772            let ix = buffer.len() - 4;
1773            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1774            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1775
1776            // keep that indent after an empty line
1777            let ix = buffer.len() - 8;
1778            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1779            assert_eq!(
1780                buffer.text(),
1781                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1782            );
1783
1784            buffer
1785        });
1786    }
1787
1788    #[test]
1789    fn test_package_name_from_pkgid() {
1790        for (input, expected) in [
1791            (
1792                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1793                "zed",
1794            ),
1795            (
1796                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1797                "my-custom-package",
1798            ),
1799        ] {
1800            assert_eq!(package_name_from_pkgid(input), Some(expected));
1801        }
1802    }
1803
1804    #[test]
1805    fn test_target_info_from_metadata() {
1806        for (input, absolute_path, expected) in [
1807            (
1808                r#"{"packages":[{"id":"path+file:///absolute/path/to/project/zed/crates/zed#0.131.0","manifest_path":"/path/to/zed/Cargo.toml","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1809                "/path/to/zed/src/main.rs",
1810                Some((
1811                    Some(TargetInfo {
1812                        package_name: "zed".into(),
1813                        target_name: "zed".into(),
1814                        required_features: Vec::new(),
1815                        target_kind: TargetKind::Bin,
1816                    }),
1817                    Arc::from("/path/to/zed".as_ref()),
1818                )),
1819            ),
1820            (
1821                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","manifest_path":"/path/to/custom-package/Cargo.toml","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1822                "/path/to/custom-package/src/main.rs",
1823                Some((
1824                    Some(TargetInfo {
1825                        package_name: "my-custom-package".into(),
1826                        target_name: "my-custom-bin".into(),
1827                        required_features: Vec::new(),
1828                        target_kind: TargetKind::Bin,
1829                    }),
1830                    Arc::from("/path/to/custom-package".as_ref()),
1831                )),
1832            ),
1833            (
1834                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"}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1835                "/path/to/custom-package/src/main.rs",
1836                Some((
1837                    Some(TargetInfo {
1838                        package_name: "my-custom-package".into(),
1839                        target_name: "my-custom-bin".into(),
1840                        required_features: Vec::new(),
1841                        target_kind: TargetKind::Example,
1842                    }),
1843                    Arc::from("/path/to/custom-package".as_ref()),
1844                )),
1845            ),
1846            (
1847                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","manifest_path":"/path/to/custom-package/Cargo.toml","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs","required-features":["foo","bar"]}]}]}"#,
1848                "/path/to/custom-package/src/main.rs",
1849                Some((
1850                    Some(TargetInfo {
1851                        package_name: "my-custom-package".into(),
1852                        target_name: "my-custom-bin".into(),
1853                        required_features: vec!["foo".to_owned(), "bar".to_owned()],
1854                        target_kind: TargetKind::Example,
1855                    }),
1856                    Arc::from("/path/to/custom-package".as_ref()),
1857                )),
1858            ),
1859            (
1860                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","required-features":[]}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1861                "/path/to/custom-package/src/main.rs",
1862                Some((
1863                    Some(TargetInfo {
1864                        package_name: "my-custom-package".into(),
1865                        target_name: "my-custom-bin".into(),
1866                        required_features: vec![],
1867                        target_kind: TargetKind::Example,
1868                    }),
1869                    Arc::from("/path/to/custom-package".as_ref()),
1870                )),
1871            ),
1872            (
1873                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"}],"manifest_path":"/path/to/custom-package/Cargo.toml"}]}"#,
1874                "/path/to/custom-package/src/main.rs",
1875                Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1876            ),
1877        ] {
1878            let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1879
1880            let absolute_path = Path::new(absolute_path);
1881
1882            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1883        }
1884    }
1885
1886    #[test]
1887    fn test_rust_test_fragment() {
1888        #[track_caller]
1889        fn check(
1890            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1891            path: &str,
1892            expected: &str,
1893        ) {
1894            let path = Path::new(path);
1895            let found = test_fragment(
1896                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1897                path,
1898                path.file_stem().unwrap().to_str().unwrap(),
1899            );
1900            assert_eq!(expected, found);
1901        }
1902
1903        check([], "/project/src/lib.rs", "--lib");
1904        check([], "/project/src/foo/mod.rs", "foo");
1905        check(
1906            [
1907                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1908                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1909            ],
1910            "/project/src/main.rs",
1911            "--bin=x",
1912        );
1913        check([], "/project/src/main.rs", "--");
1914    }
1915}