rust.rs

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