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