rust.rs

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