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