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::fetch_github_binary_with_digest_check;
   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 metadata_path = destination_path.with_extension("metadata");
 518
 519        let server_path_for_check = server_path.clone();
 520        fetch_github_binary_with_digest_check(
 521            &server_path,
 522            &metadata_path,
 523            expected_digest,
 524            &url,
 525            Self::GITHUB_ASSET_KIND,
 526            &destination_path,
 527            &*delegate.http_client(),
 528            || async move {
 529                delegate
 530                    .try_exec(LanguageServerBinary {
 531                        path: server_path_for_check,
 532                        arguments: vec!["--version".into()],
 533                        env: None,
 534                    })
 535                    .await
 536                    .inspect_err(|err| {
 537                        log::warn!("Unable to run rust-analyzer asset, redownloading: {err:#}")
 538                    })
 539            },
 540        )
 541        .await?;
 542
 543        make_file_executable(&server_path).await?;
 544        remove_matching(&container_dir, |path| path != destination_path).await;
 545
 546        Ok(LanguageServerBinary {
 547            path: server_path,
 548            env: None,
 549            arguments: Default::default(),
 550        })
 551    }
 552
 553    async fn cached_server_binary(
 554        &self,
 555        container_dir: PathBuf,
 556        _: &dyn LspAdapterDelegate,
 557    ) -> Option<LanguageServerBinary> {
 558        get_cached_server_binary(container_dir).await
 559    }
 560}
 561
 562pub(crate) struct RustContextProvider;
 563
 564const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 565    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 566
 567/// The bin name corresponding to the current file in Cargo.toml
 568const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 569    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 570
 571/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 572const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 573    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 574
 575/// The flag to list required features for executing a bin, if any
 576const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
 577    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
 578
 579/// The list of required features for executing a bin, if any
 580const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
 581    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
 582
 583const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 584    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 585
 586const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 587    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 588
 589const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 590    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 591
 592const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
 593    VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
 594
 595impl ContextProvider for RustContextProvider {
 596    fn build_context(
 597        &self,
 598        task_variables: &TaskVariables,
 599        location: ContextLocation<'_>,
 600        project_env: Option<HashMap<String, String>>,
 601        _: Arc<dyn LanguageToolchainStore>,
 602        cx: &mut gpui::App,
 603    ) -> Task<Result<TaskVariables>> {
 604        let local_abs_path = location
 605            .file_location
 606            .buffer
 607            .read(cx)
 608            .file()
 609            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 610
 611        let mut variables = TaskVariables::default();
 612
 613        if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
 614        {
 615            let fragment = test_fragment(&variables, path, stem);
 616            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 617        };
 618        if let Some(test_name) =
 619            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 620        {
 621            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 622        }
 623        if let Some(doc_test_name) =
 624            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 625        {
 626            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 627        }
 628        cx.background_spawn(async move {
 629            if let Some(path) = local_abs_path
 630                .as_deref()
 631                .and_then(|local_abs_path| local_abs_path.parent())
 632                && let Some(package_name) =
 633                    human_readable_package_name(path, project_env.as_ref()).await
 634            {
 635                variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 636            }
 637            if let Some(path) = local_abs_path.as_ref()
 638                && let Some((target, manifest_path)) =
 639                    target_info_from_abs_path(path, project_env.as_ref()).await
 640            {
 641                if let Some(target) = target {
 642                    variables.extend(TaskVariables::from_iter([
 643                        (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 644                        (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 645                        (
 646                            RUST_BIN_KIND_TASK_VARIABLE.clone(),
 647                            target.target_kind.to_string(),
 648                        ),
 649                    ]));
 650                    if target.required_features.is_empty() {
 651                        variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
 652                        variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
 653                    } else {
 654                        variables.insert(
 655                            RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
 656                            "--features".to_string(),
 657                        );
 658                        variables.insert(
 659                            RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
 660                            target.required_features.join(","),
 661                        );
 662                    }
 663                }
 664                variables.extend(TaskVariables::from_iter([(
 665                    RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
 666                    manifest_path.to_string_lossy().into_owned(),
 667                )]));
 668            }
 669            Ok(variables)
 670        })
 671    }
 672
 673    fn associated_tasks(
 674        &self,
 675        file: Option<Arc<dyn language::File>>,
 676        cx: &App,
 677    ) -> Task<Option<TaskTemplates>> {
 678        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 679        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 680
 681        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 682        let package_to_run = language_sets
 683            .tasks
 684            .variables
 685            .get(DEFAULT_RUN_NAME_STR)
 686            .cloned();
 687        let custom_target_dir = language_sets
 688            .tasks
 689            .variables
 690            .get(CUSTOM_TARGET_DIR)
 691            .cloned();
 692        let run_task_args = if let Some(package_to_run) = package_to_run {
 693            vec!["run".into(), "-p".into(), package_to_run]
 694        } else {
 695            vec!["run".into()]
 696        };
 697        let mut task_templates = vec![
 698            TaskTemplate {
 699                label: format!(
 700                    "Check (package: {})",
 701                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 702                ),
 703                command: "cargo".into(),
 704                args: vec![
 705                    "check".into(),
 706                    "-p".into(),
 707                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 708                ],
 709                cwd: Some("$ZED_DIRNAME".to_owned()),
 710                ..TaskTemplate::default()
 711            },
 712            TaskTemplate {
 713                label: "Check all targets (workspace)".into(),
 714                command: "cargo".into(),
 715                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 716                cwd: Some("$ZED_DIRNAME".to_owned()),
 717                ..TaskTemplate::default()
 718            },
 719            TaskTemplate {
 720                label: format!(
 721                    "Test '{}' (package: {})",
 722                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 723                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 724                ),
 725                command: "cargo".into(),
 726                args: vec![
 727                    "test".into(),
 728                    "-p".into(),
 729                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 730                    "--".into(),
 731                    "--nocapture".into(),
 732                    "--include-ignored".into(),
 733                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 734                ],
 735                tags: vec!["rust-test".to_owned()],
 736                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 737                ..TaskTemplate::default()
 738            },
 739            TaskTemplate {
 740                label: format!(
 741                    "Doc test '{}' (package: {})",
 742                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 743                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 744                ),
 745                command: "cargo".into(),
 746                args: vec![
 747                    "test".into(),
 748                    "--doc".into(),
 749                    "-p".into(),
 750                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 751                    "--".into(),
 752                    "--nocapture".into(),
 753                    "--include-ignored".into(),
 754                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 755                ],
 756                tags: vec!["rust-doc-test".to_owned()],
 757                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 758                ..TaskTemplate::default()
 759            },
 760            TaskTemplate {
 761                label: format!(
 762                    "Test mod '{}' (package: {})",
 763                    VariableName::Stem.template_value(),
 764                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 765                ),
 766                command: "cargo".into(),
 767                args: vec![
 768                    "test".into(),
 769                    "-p".into(),
 770                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 771                    "--".into(),
 772                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 773                ],
 774                tags: vec!["rust-mod-test".to_owned()],
 775                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 776                ..TaskTemplate::default()
 777            },
 778            TaskTemplate {
 779                label: format!(
 780                    "Run {} {} (package: {})",
 781                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 782                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 783                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 784                ),
 785                command: "cargo".into(),
 786                args: vec![
 787                    "run".into(),
 788                    "-p".into(),
 789                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 790                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 791                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 792                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
 793                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
 794                ],
 795                cwd: Some("$ZED_DIRNAME".to_owned()),
 796                tags: vec!["rust-main".to_owned()],
 797                ..TaskTemplate::default()
 798            },
 799            TaskTemplate {
 800                label: format!(
 801                    "Test (package: {})",
 802                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 803                ),
 804                command: "cargo".into(),
 805                args: vec![
 806                    "test".into(),
 807                    "-p".into(),
 808                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 809                ],
 810                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 811                ..TaskTemplate::default()
 812            },
 813            TaskTemplate {
 814                label: "Run".into(),
 815                command: "cargo".into(),
 816                args: run_task_args,
 817                cwd: Some("$ZED_DIRNAME".to_owned()),
 818                ..TaskTemplate::default()
 819            },
 820            TaskTemplate {
 821                label: "Clean".into(),
 822                command: "cargo".into(),
 823                args: vec!["clean".into()],
 824                cwd: Some("$ZED_DIRNAME".to_owned()),
 825                ..TaskTemplate::default()
 826            },
 827        ];
 828
 829        if let Some(custom_target_dir) = custom_target_dir {
 830            task_templates = task_templates
 831                .into_iter()
 832                .map(|mut task_template| {
 833                    let mut args = task_template.args.split_off(1);
 834                    task_template.args.append(&mut vec![
 835                        "--target-dir".to_string(),
 836                        custom_target_dir.clone(),
 837                    ]);
 838                    task_template.args.append(&mut args);
 839
 840                    task_template
 841                })
 842                .collect();
 843        }
 844
 845        Task::ready(Some(TaskTemplates(task_templates)))
 846    }
 847
 848    fn lsp_task_source(&self) -> Option<LanguageServerName> {
 849        Some(SERVER_NAME)
 850    }
 851}
 852
 853/// Part of the data structure of Cargo metadata
 854#[derive(Debug, serde::Deserialize)]
 855struct CargoMetadata {
 856    packages: Vec<CargoPackage>,
 857}
 858
 859#[derive(Debug, serde::Deserialize)]
 860struct CargoPackage {
 861    id: String,
 862    targets: Vec<CargoTarget>,
 863    manifest_path: Arc<Path>,
 864}
 865
 866#[derive(Debug, serde::Deserialize)]
 867struct CargoTarget {
 868    name: String,
 869    kind: Vec<String>,
 870    src_path: String,
 871    #[serde(rename = "required-features", default)]
 872    required_features: Vec<String>,
 873}
 874
 875#[derive(Debug, PartialEq)]
 876enum TargetKind {
 877    Bin,
 878    Example,
 879}
 880
 881impl Display for TargetKind {
 882    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 883        match self {
 884            TargetKind::Bin => write!(f, "bin"),
 885            TargetKind::Example => write!(f, "example"),
 886        }
 887    }
 888}
 889
 890impl TryFrom<&str> for TargetKind {
 891    type Error = ();
 892    fn try_from(value: &str) -> Result<Self, ()> {
 893        match value {
 894            "bin" => Ok(Self::Bin),
 895            "example" => Ok(Self::Example),
 896            _ => Err(()),
 897        }
 898    }
 899}
 900/// Which package and binary target are we in?
 901#[derive(Debug, PartialEq)]
 902struct TargetInfo {
 903    package_name: String,
 904    target_name: String,
 905    target_kind: TargetKind,
 906    required_features: Vec<String>,
 907}
 908
 909async fn target_info_from_abs_path(
 910    abs_path: &Path,
 911    project_env: Option<&HashMap<String, String>>,
 912) -> Option<(Option<TargetInfo>, Arc<Path>)> {
 913    let mut command = util::command::new_smol_command("cargo");
 914    if let Some(envs) = project_env {
 915        command.envs(envs);
 916    }
 917    let output = command
 918        .current_dir(abs_path.parent()?)
 919        .arg("metadata")
 920        .arg("--no-deps")
 921        .arg("--format-version")
 922        .arg("1")
 923        .output()
 924        .await
 925        .log_err()?
 926        .stdout;
 927
 928    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
 929    target_info_from_metadata(metadata, abs_path)
 930}
 931
 932fn target_info_from_metadata(
 933    metadata: CargoMetadata,
 934    abs_path: &Path,
 935) -> Option<(Option<TargetInfo>, Arc<Path>)> {
 936    let mut manifest_path = None;
 937    for package in metadata.packages {
 938        let Some(manifest_dir_path) = package.manifest_path.parent() else {
 939            continue;
 940        };
 941
 942        let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
 943            continue;
 944        };
 945        let candidate_path_length = path_from_manifest_dir.components().count();
 946        // Pick the most specific manifest path
 947        if let Some((path, current_length)) = &mut manifest_path {
 948            if candidate_path_length > *current_length {
 949                *path = Arc::from(manifest_dir_path);
 950                *current_length = candidate_path_length;
 951            }
 952        } else {
 953            manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
 954        };
 955
 956        for target in package.targets {
 957            let Some(bin_kind) = target
 958                .kind
 959                .iter()
 960                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
 961            else {
 962                continue;
 963            };
 964            let target_path = PathBuf::from(target.src_path);
 965            if target_path == abs_path {
 966                return manifest_path.map(|(path, _)| {
 967                    (
 968                        package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
 969                            package_name: package_name.to_owned(),
 970                            target_name: target.name,
 971                            required_features: target.required_features,
 972                            target_kind: bin_kind,
 973                        }),
 974                        path,
 975                    )
 976                });
 977            }
 978        }
 979    }
 980
 981    manifest_path.map(|(path, _)| (None, path))
 982}
 983
 984async fn human_readable_package_name(
 985    package_directory: &Path,
 986    project_env: Option<&HashMap<String, String>>,
 987) -> Option<String> {
 988    let mut command = util::command::new_smol_command("cargo");
 989    if let Some(envs) = project_env {
 990        command.envs(envs);
 991    }
 992    let pkgid = String::from_utf8(
 993        command
 994            .current_dir(package_directory)
 995            .arg("pkgid")
 996            .output()
 997            .await
 998            .log_err()?
 999            .stdout,
1000    )
1001    .ok()?;
1002    Some(package_name_from_pkgid(&pkgid)?.to_owned())
1003}
1004
1005// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1006// Output example in the root of Zed project:
1007// ```sh
1008// ❯ cargo pkgid zed
1009// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1010// ```
1011// Another variant, if a project has a custom package name or hyphen in the name:
1012// ```
1013// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1014// ```
1015//
1016// Extracts the package name from the output according to the spec:
1017// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1018fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1019    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1020        match input.rsplit_once(suffix_start) {
1021            Some((without_suffix, _)) => without_suffix,
1022            None => input,
1023        }
1024    }
1025
1026    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1027    let package_name = match version_suffix.rsplit_once('@') {
1028        Some((custom_package_name, _version)) => custom_package_name,
1029        None => {
1030            let host_and_path = split_off_suffix(version_prefix, '?');
1031            let (_, package_name) = host_and_path.rsplit_once('/')?;
1032            package_name
1033        }
1034    };
1035    Some(package_name)
1036}
1037
1038async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1039    maybe!(async {
1040        let mut last = None;
1041        let mut entries = fs::read_dir(&container_dir).await?;
1042        while let Some(entry) = entries.next().await {
1043            let path = entry?.path();
1044            if path.extension().is_some_and(|ext| ext == "metadata") {
1045                continue;
1046            }
1047            last = Some(path);
1048        }
1049
1050        let path = last.context("no cached binary")?;
1051        let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1052            AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1053            AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1054        };
1055
1056        anyhow::Ok(LanguageServerBinary {
1057            path,
1058            env: None,
1059            arguments: Default::default(),
1060        })
1061    })
1062    .await
1063    .log_err()
1064}
1065
1066fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1067    let fragment = if stem == "lib" {
1068        // This isn't quite right---it runs the tests for the entire library, rather than
1069        // just for the top-level `mod tests`. But we don't really have the means here to
1070        // filter out just that module.
1071        Some("--lib".to_owned())
1072    } else if stem == "mod" {
1073        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1074    } else if stem == "main" {
1075        if let (Some(bin_name), Some(bin_kind)) = (
1076            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1077            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1078        ) {
1079            Some(format!("--{bin_kind}={bin_name}"))
1080        } else {
1081            None
1082        }
1083    } else {
1084        Some(stem.to_owned())
1085    };
1086    fragment.unwrap_or_else(|| "--".to_owned())
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use std::num::NonZeroU32;
1092
1093    use super::*;
1094    use crate::language;
1095    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1096    use lsp::CompletionItemLabelDetails;
1097    use settings::SettingsStore;
1098    use theme::SyntaxTheme;
1099    use util::path;
1100
1101    #[gpui::test]
1102    async fn test_process_rust_diagnostics() {
1103        let mut params = lsp::PublishDiagnosticsParams {
1104            uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1105            version: None,
1106            diagnostics: vec![
1107                // no newlines
1108                lsp::Diagnostic {
1109                    message: "use of moved value `a`".to_string(),
1110                    ..Default::default()
1111                },
1112                // newline at the end of a code span
1113                lsp::Diagnostic {
1114                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1115                    ..Default::default()
1116                },
1117                // code span starting right after a newline
1118                lsp::Diagnostic {
1119                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1120                        .to_string(),
1121                    ..Default::default()
1122                },
1123            ],
1124        };
1125        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1126
1127        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1128
1129        // remove trailing newline from code span
1130        assert_eq!(
1131            params.diagnostics[1].message,
1132            "consider importing this struct: `use b::c;`"
1133        );
1134
1135        // do not remove newline before the start of code span
1136        assert_eq!(
1137            params.diagnostics[2].message,
1138            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1139        );
1140    }
1141
1142    #[gpui::test]
1143    async fn test_rust_label_for_completion() {
1144        let adapter = Arc::new(RustLspAdapter);
1145        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1146        let grammar = language.grammar().unwrap();
1147        let theme = SyntaxTheme::new_test([
1148            ("type", Hsla::default()),
1149            ("keyword", Hsla::default()),
1150            ("function", Hsla::default()),
1151            ("property", Hsla::default()),
1152        ]);
1153
1154        language.set_theme(&theme);
1155
1156        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1157        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1158        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1159        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1160
1161        assert_eq!(
1162            adapter
1163                .label_for_completion(
1164                    &lsp::CompletionItem {
1165                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1166                        label: "hello(…)".to_string(),
1167                        label_details: Some(CompletionItemLabelDetails {
1168                            detail: Some("(use crate::foo)".into()),
1169                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1170                        }),
1171                        ..Default::default()
1172                    },
1173                    &language
1174                )
1175                .await,
1176            Some(CodeLabel::new(
1177                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1178                0..5,
1179                vec![
1180                    (0..5, highlight_function),
1181                    (7..10, highlight_keyword),
1182                    (11..17, highlight_type),
1183                    (18..19, highlight_type),
1184                    (25..28, highlight_type),
1185                    (29..30, highlight_type),
1186                ],
1187            ))
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("async 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::FIELD),
1222                        label: "len".to_string(),
1223                        detail: Some("usize".to_string()),
1224                        ..Default::default()
1225                    },
1226                    &language
1227                )
1228                .await,
1229            Some(CodeLabel::new(
1230                "len: usize".to_string(),
1231                0..3,
1232                vec![(0..3, highlight_field), (5..10, highlight_type),],
1233            ))
1234        );
1235
1236        assert_eq!(
1237            adapter
1238                .label_for_completion(
1239                    &lsp::CompletionItem {
1240                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1241                        label: "hello(…)".to_string(),
1242                        label_details: Some(CompletionItemLabelDetails {
1243                            detail: Some("(use crate::foo)".to_string()),
1244                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1245                        }),
1246
1247                        ..Default::default()
1248                    },
1249                    &language
1250                )
1251                .await,
1252            Some(CodeLabel::new(
1253                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1254                0..5,
1255                vec![
1256                    (0..5, highlight_function),
1257                    (7..10, highlight_keyword),
1258                    (11..17, highlight_type),
1259                    (18..19, highlight_type),
1260                    (25..28, highlight_type),
1261                    (29..30, highlight_type),
1262                ],
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                        ..Default::default()
1277                    },
1278                    &language
1279                )
1280                .await,
1281            Some(CodeLabel::new(
1282                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1283                0..5,
1284                vec![
1285                    (0..5, highlight_function),
1286                    (7..10, highlight_keyword),
1287                    (11..17, highlight_type),
1288                    (18..19, highlight_type),
1289                    (25..28, highlight_type),
1290                    (29..30, highlight_type),
1291                ],
1292            ))
1293        );
1294
1295        assert_eq!(
1296            adapter
1297                .label_for_completion(
1298                    &lsp::CompletionItem {
1299                        kind: Some(lsp::CompletionItemKind::METHOD),
1300                        label: "await.as_deref_mut()".to_string(),
1301                        filter_text: Some("as_deref_mut".to_string()),
1302                        label_details: Some(CompletionItemLabelDetails {
1303                            detail: None,
1304                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1305                        }),
1306                        ..Default::default()
1307                    },
1308                    &language
1309                )
1310                .await,
1311            Some(CodeLabel::new(
1312                "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1313                6..18,
1314                vec![
1315                    (6..18, HighlightId(2)),
1316                    (20..23, HighlightId(1)),
1317                    (33..40, HighlightId(0)),
1318                    (45..46, HighlightId(0))
1319                ],
1320            ))
1321        );
1322
1323        assert_eq!(
1324            adapter
1325                .label_for_completion(
1326                    &lsp::CompletionItem {
1327                        kind: Some(lsp::CompletionItemKind::METHOD),
1328                        label: "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(
1333                                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1334                            ),
1335                        }),
1336                        ..Default::default()
1337                    },
1338                    &language
1339                )
1340                .await,
1341            Some(CodeLabel::new(
1342                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1343                7..19,
1344                vec![
1345                    (0..3, HighlightId(1)),
1346                    (4..6, HighlightId(1)),
1347                    (7..19, HighlightId(2)),
1348                    (21..24, HighlightId(1)),
1349                    (34..41, HighlightId(0)),
1350                    (46..47, HighlightId(0))
1351                ],
1352            ))
1353        );
1354
1355        assert_eq!(
1356            adapter
1357                .label_for_completion(
1358                    &lsp::CompletionItem {
1359                        kind: Some(lsp::CompletionItemKind::FIELD),
1360                        label: "inner_value".to_string(),
1361                        filter_text: Some("value".to_string()),
1362                        detail: Some("String".to_string()),
1363                        ..Default::default()
1364                    },
1365                    &language,
1366                )
1367                .await,
1368            Some(CodeLabel::new(
1369                "inner_value: String".to_string(),
1370                6..11,
1371                vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1372            ))
1373        );
1374    }
1375
1376    #[gpui::test]
1377    async fn test_rust_label_for_symbol() {
1378        let adapter = Arc::new(RustLspAdapter);
1379        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1380        let grammar = language.grammar().unwrap();
1381        let theme = SyntaxTheme::new_test([
1382            ("type", Hsla::default()),
1383            ("keyword", Hsla::default()),
1384            ("function", Hsla::default()),
1385            ("property", Hsla::default()),
1386        ]);
1387
1388        language.set_theme(&theme);
1389
1390        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1391        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1392        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1393
1394        assert_eq!(
1395            adapter
1396                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1397                .await,
1398            Some(CodeLabel::new(
1399                "fn hello".to_string(),
1400                3..8,
1401                vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1402            ))
1403        );
1404
1405        assert_eq!(
1406            adapter
1407                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1408                .await,
1409            Some(CodeLabel::new(
1410                "type World".to_string(),
1411                5..10,
1412                vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1413            ))
1414        );
1415    }
1416
1417    #[gpui::test]
1418    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1419        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1420        cx.update(|cx| {
1421            let test_settings = SettingsStore::test(cx);
1422            cx.set_global(test_settings);
1423            cx.update_global::<SettingsStore, _>(|store, cx| {
1424                store.update_user_settings(cx, |s| {
1425                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1426                });
1427            });
1428        });
1429
1430        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1431
1432        cx.new(|cx| {
1433            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1434
1435            // indent between braces
1436            buffer.set_text("fn a() {}", cx);
1437            let ix = buffer.len() - 1;
1438            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1439            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1440
1441            // indent between braces, even after empty lines
1442            buffer.set_text("fn a() {\n\n\n}", cx);
1443            let ix = buffer.len() - 2;
1444            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1445            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1446
1447            // indent a line that continues a field expression
1448            buffer.set_text("fn a() {\n  \n}", cx);
1449            let ix = buffer.len() - 2;
1450            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1451            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1452
1453            // indent further lines that continue the field expression, even after empty lines
1454            let ix = buffer.len() - 2;
1455            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1456            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1457
1458            // dedent the line after the field expression
1459            let ix = buffer.len() - 2;
1460            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1461            assert_eq!(
1462                buffer.text(),
1463                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1464            );
1465
1466            // indent inside a struct within a call
1467            buffer.set_text("const a: B = c(D {});", cx);
1468            let ix = buffer.len() - 3;
1469            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1470            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1471
1472            // indent further inside a nested call
1473            let ix = buffer.len() - 4;
1474            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1475            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1476
1477            // keep that indent after an empty line
1478            let ix = buffer.len() - 8;
1479            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1480            assert_eq!(
1481                buffer.text(),
1482                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1483            );
1484
1485            buffer
1486        });
1487    }
1488
1489    #[test]
1490    fn test_package_name_from_pkgid() {
1491        for (input, expected) in [
1492            (
1493                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1494                "zed",
1495            ),
1496            (
1497                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1498                "my-custom-package",
1499            ),
1500        ] {
1501            assert_eq!(package_name_from_pkgid(input), Some(expected));
1502        }
1503    }
1504
1505    #[test]
1506    fn test_target_info_from_metadata() {
1507        for (input, absolute_path, expected) in [
1508            (
1509                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"}]}]}"#,
1510                "/path/to/zed/src/main.rs",
1511                Some((
1512                    Some(TargetInfo {
1513                        package_name: "zed".into(),
1514                        target_name: "zed".into(),
1515                        required_features: Vec::new(),
1516                        target_kind: TargetKind::Bin,
1517                    }),
1518                    Arc::from("/path/to/zed".as_ref()),
1519                )),
1520            ),
1521            (
1522                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"}]}]}"#,
1523                "/path/to/custom-package/src/main.rs",
1524                Some((
1525                    Some(TargetInfo {
1526                        package_name: "my-custom-package".into(),
1527                        target_name: "my-custom-bin".into(),
1528                        required_features: Vec::new(),
1529                        target_kind: TargetKind::Bin,
1530                    }),
1531                    Arc::from("/path/to/custom-package".as_ref()),
1532                )),
1533            ),
1534            (
1535                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"}]}"#,
1536                "/path/to/custom-package/src/main.rs",
1537                Some((
1538                    Some(TargetInfo {
1539                        package_name: "my-custom-package".into(),
1540                        target_name: "my-custom-bin".into(),
1541                        required_features: Vec::new(),
1542                        target_kind: TargetKind::Example,
1543                    }),
1544                    Arc::from("/path/to/custom-package".as_ref()),
1545                )),
1546            ),
1547            (
1548                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"]}]}]}"#,
1549                "/path/to/custom-package/src/main.rs",
1550                Some((
1551                    Some(TargetInfo {
1552                        package_name: "my-custom-package".into(),
1553                        target_name: "my-custom-bin".into(),
1554                        required_features: vec!["foo".to_owned(), "bar".to_owned()],
1555                        target_kind: TargetKind::Example,
1556                    }),
1557                    Arc::from("/path/to/custom-package".as_ref()),
1558                )),
1559            ),
1560            (
1561                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"}]}"#,
1562                "/path/to/custom-package/src/main.rs",
1563                Some((
1564                    Some(TargetInfo {
1565                        package_name: "my-custom-package".into(),
1566                        target_name: "my-custom-bin".into(),
1567                        required_features: vec![],
1568                        target_kind: TargetKind::Example,
1569                    }),
1570                    Arc::from("/path/to/custom-package".as_ref()),
1571                )),
1572            ),
1573            (
1574                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"}]}"#,
1575                "/path/to/custom-package/src/main.rs",
1576                Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1577            ),
1578        ] {
1579            let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1580
1581            let absolute_path = Path::new(absolute_path);
1582
1583            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1584        }
1585    }
1586
1587    #[test]
1588    fn test_rust_test_fragment() {
1589        #[track_caller]
1590        fn check(
1591            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1592            path: &str,
1593            expected: &str,
1594        ) {
1595            let path = Path::new(path);
1596            let found = test_fragment(
1597                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1598                path,
1599                path.file_stem().unwrap().to_str().unwrap(),
1600            );
1601            assert_eq!(expected, found);
1602        }
1603
1604        check([], "/project/src/lib.rs", "--lib");
1605        check([], "/project/src/foo/mod.rs", "foo");
1606        check(
1607            [
1608                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1609                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1610            ],
1611            "/project/src/main.rs",
1612            "--bin=x",
1613        );
1614        check([], "/project/src/main.rs", "--");
1615    }
1616}