rust.rs

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