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..start_pos]);
 379
 380                        if start_pos == end_pos {
 381                            let caret_start = label.len();
 382                            label.push('…');
 383                            runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
 384                        } else {
 385                            let label_start = label.len();
 386                            label.push_str(&snippet.text[start_pos..end_pos]);
 387                            let label_end = label.len();
 388                            runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
 389                        }
 390
 391                        text_pos = end_pos;
 392                    }
 393
 394                    label.push_str(&snippet.text[text_pos..]);
 395
 396                    if detail_left.is_some_and(|detail_left| detail_left == new_text) {
 397                        // We only include the left detail if it isn't the snippet again
 398                        detail_left.take();
 399                    }
 400
 401                    runs.extend(language.highlight_text(&Rope::from(&label), 0..label.len()));
 402                } else {
 403                    let highlight_name = kind.and_then(|kind| match kind {
 404                        lsp::CompletionItemKind::STRUCT
 405                        | lsp::CompletionItemKind::INTERFACE
 406                        | lsp::CompletionItemKind::ENUM => Some("type"),
 407                        lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 408                        lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 409                        lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 410                            Some("constant")
 411                        }
 412                        _ => None,
 413                    });
 414
 415                    label = completion.label.clone();
 416
 417                    if let Some(highlight_name) = highlight_name {
 418                        let highlight_id =
 419                            language.grammar()?.highlight_id_for_name(highlight_name)?;
 420                        runs.push((
 421                            0..label.rfind('(').unwrap_or(completion.label.len()),
 422                            highlight_id,
 423                        ));
 424                    } else if detail_left.is_none()
 425                        && kind != Some(lsp::CompletionItemKind::SNIPPET)
 426                    {
 427                        return None;
 428                    }
 429                }
 430
 431                let label_len = label.len();
 432
 433                mk_label(label, &|| 0..label_len, runs)
 434            }
 435        };
 436
 437        if let Some(detail_left) = detail_left {
 438            label.text.push(' ');
 439            if !detail_left.starts_with('(') {
 440                label.text.push('(');
 441            }
 442            label.text.push_str(detail_left);
 443            if !detail_left.ends_with(')') {
 444                label.text.push(')');
 445            }
 446        }
 447
 448        Some(label)
 449    }
 450
 451    async fn label_for_symbol(
 452        &self,
 453        name: &str,
 454        kind: lsp::SymbolKind,
 455        language: &Arc<Language>,
 456    ) -> Option<CodeLabel> {
 457        let (prefix, suffix) = match kind {
 458            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"),
 459            lsp::SymbolKind::STRUCT => ("struct ", " {}"),
 460            lsp::SymbolKind::ENUM => ("enum ", " {}"),
 461            lsp::SymbolKind::INTERFACE => ("trait ", " {}"),
 462            lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"),
 463            lsp::SymbolKind::MODULE => ("mod ", " {}"),
 464            lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"),
 465            _ => return None,
 466        };
 467
 468        let filter_range = prefix.len()..prefix.len() + name.len();
 469        let display_range = 0..filter_range.end;
 470        Some(CodeLabel::new(
 471            format!("{prefix}{name}"),
 472            filter_range,
 473            language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
 474        ))
 475    }
 476
 477    fn prepare_initialize_params(
 478        &self,
 479        mut original: InitializeParams,
 480        cx: &App,
 481    ) -> Result<InitializeParams> {
 482        let enable_lsp_tasks = ProjectSettings::get_global(cx)
 483            .lsp
 484            .get(&SERVER_NAME)
 485            .is_some_and(|s| s.enable_lsp_tasks);
 486        if enable_lsp_tasks {
 487            let experimental = json!({
 488                "runnables": {
 489                    "kinds": [ "cargo", "shell" ],
 490                },
 491            });
 492            if let Some(original_experimental) = &mut original.capabilities.experimental {
 493                merge_json_value_into(experimental, original_experimental);
 494            } else {
 495                original.capabilities.experimental = Some(experimental);
 496            }
 497        }
 498
 499        Ok(original)
 500    }
 501}
 502
 503impl LspInstaller for RustLspAdapter {
 504    type BinaryVersion = GitHubLspBinaryVersion;
 505    async fn check_if_user_installed(
 506        &self,
 507        delegate: &dyn LspAdapterDelegate,
 508        _: Option<Toolchain>,
 509        _: &AsyncApp,
 510    ) -> Option<LanguageServerBinary> {
 511        let path = delegate.which("rust-analyzer".as_ref()).await?;
 512        let env = delegate.shell_env().await;
 513
 514        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
 515        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
 516        log::debug!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
 517        let result = delegate
 518            .try_exec(LanguageServerBinary {
 519                path: path.clone(),
 520                arguments: vec!["--help".into()],
 521                env: Some(env.clone()),
 522            })
 523            .await;
 524        if let Err(err) = result {
 525            log::debug!(
 526                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
 527                path,
 528                err
 529            );
 530            return None;
 531        }
 532
 533        Some(LanguageServerBinary {
 534            path,
 535            env: Some(env),
 536            arguments: vec![],
 537        })
 538    }
 539
 540    async fn fetch_latest_server_version(
 541        &self,
 542        delegate: &dyn LspAdapterDelegate,
 543        pre_release: bool,
 544        _: &mut AsyncApp,
 545    ) -> Result<GitHubLspBinaryVersion> {
 546        let release = latest_github_release(
 547            "rust-lang/rust-analyzer",
 548            true,
 549            pre_release,
 550            delegate.http_client(),
 551        )
 552        .await?;
 553        let asset_name = Self::build_asset_name().await;
 554        let asset = release
 555            .assets
 556            .into_iter()
 557            .find(|asset| asset.name == asset_name)
 558            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 559        Ok(GitHubLspBinaryVersion {
 560            name: release.tag_name,
 561            url: asset.browser_download_url,
 562            digest: asset.digest,
 563        })
 564    }
 565
 566    async fn fetch_server_binary(
 567        &self,
 568        version: GitHubLspBinaryVersion,
 569        container_dir: PathBuf,
 570        delegate: &dyn LspAdapterDelegate,
 571    ) -> Result<LanguageServerBinary> {
 572        let GitHubLspBinaryVersion {
 573            name,
 574            url,
 575            digest: expected_digest,
 576        } = version;
 577        let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
 578        let server_path = match Self::GITHUB_ASSET_KIND {
 579            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 580            AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
 581        };
 582
 583        let binary = LanguageServerBinary {
 584            path: server_path.clone(),
 585            env: None,
 586            arguments: Default::default(),
 587        };
 588
 589        let metadata_path = destination_path.with_extension("metadata");
 590        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
 591            .await
 592            .ok();
 593        if let Some(metadata) = metadata {
 594            let validity_check = async || {
 595                delegate
 596                    .try_exec(LanguageServerBinary {
 597                        path: server_path.clone(),
 598                        arguments: vec!["--version".into()],
 599                        env: None,
 600                    })
 601                    .await
 602                    .inspect_err(|err| {
 603                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
 604                    })
 605            };
 606            if let (Some(actual_digest), Some(expected_digest)) =
 607                (&metadata.digest, &expected_digest)
 608            {
 609                if actual_digest == expected_digest {
 610                    if validity_check().await.is_ok() {
 611                        return Ok(binary);
 612                    }
 613                } else {
 614                    log::info!(
 615                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
 616                    );
 617                }
 618            } else if validity_check().await.is_ok() {
 619                return Ok(binary);
 620            }
 621        }
 622
 623        download_server_binary(
 624            &*delegate.http_client(),
 625            &url,
 626            expected_digest.as_deref(),
 627            &destination_path,
 628            Self::GITHUB_ASSET_KIND,
 629        )
 630        .await?;
 631        make_file_executable(&server_path).await?;
 632        remove_matching(&container_dir, |path| path != destination_path).await;
 633        GithubBinaryMetadata::write_to_file(
 634            &GithubBinaryMetadata {
 635                metadata_version: 1,
 636                digest: expected_digest,
 637            },
 638            &metadata_path,
 639        )
 640        .await?;
 641
 642        Ok(LanguageServerBinary {
 643            path: server_path,
 644            env: None,
 645            arguments: Default::default(),
 646        })
 647    }
 648
 649    async fn cached_server_binary(
 650        &self,
 651        container_dir: PathBuf,
 652        _: &dyn LspAdapterDelegate,
 653    ) -> Option<LanguageServerBinary> {
 654        get_cached_server_binary(container_dir).await
 655    }
 656}
 657
 658pub(crate) struct RustContextProvider;
 659
 660const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 661    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 662
 663/// The bin name corresponding to the current file in Cargo.toml
 664const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 665    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 666
 667/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 668const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 669    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 670
 671/// The flag to list required features for executing a bin, if any
 672const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
 673    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
 674
 675/// The list of required features for executing a bin, if any
 676const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
 677    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
 678
 679const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 680    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 681
 682const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 683    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 684
 685const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 686    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 687
 688const RUST_MANIFEST_DIRNAME_TASK_VARIABLE: VariableName =
 689    VariableName::Custom(Cow::Borrowed("RUST_MANIFEST_DIRNAME"));
 690
 691impl ContextProvider for RustContextProvider {
 692    fn build_context(
 693        &self,
 694        task_variables: &TaskVariables,
 695        location: ContextLocation<'_>,
 696        project_env: Option<HashMap<String, String>>,
 697        _: Arc<dyn LanguageToolchainStore>,
 698        cx: &mut gpui::App,
 699    ) -> Task<Result<TaskVariables>> {
 700        let local_abs_path = location
 701            .file_location
 702            .buffer
 703            .read(cx)
 704            .file()
 705            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 706
 707        let mut variables = TaskVariables::default();
 708
 709        if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
 710        {
 711            let fragment = test_fragment(&variables, path, stem);
 712            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 713        };
 714        if let Some(test_name) =
 715            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 716        {
 717            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 718        }
 719        if let Some(doc_test_name) =
 720            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 721        {
 722            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 723        }
 724        cx.background_spawn(async move {
 725            if let Some(path) = local_abs_path
 726                .as_deref()
 727                .and_then(|local_abs_path| local_abs_path.parent())
 728                && let Some(package_name) =
 729                    human_readable_package_name(path, project_env.as_ref()).await
 730            {
 731                variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 732            }
 733            if let Some(path) = local_abs_path.as_ref()
 734                && let Some((target, manifest_path)) =
 735                    target_info_from_abs_path(path, project_env.as_ref()).await
 736            {
 737                if let Some(target) = target {
 738                    variables.extend(TaskVariables::from_iter([
 739                        (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 740                        (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 741                        (
 742                            RUST_BIN_KIND_TASK_VARIABLE.clone(),
 743                            target.target_kind.to_string(),
 744                        ),
 745                    ]));
 746                    if target.required_features.is_empty() {
 747                        variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
 748                        variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
 749                    } else {
 750                        variables.insert(
 751                            RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
 752                            "--features".to_string(),
 753                        );
 754                        variables.insert(
 755                            RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
 756                            target.required_features.join(","),
 757                        );
 758                    }
 759                }
 760                variables.extend(TaskVariables::from_iter([(
 761                    RUST_MANIFEST_DIRNAME_TASK_VARIABLE.clone(),
 762                    manifest_path.to_string_lossy().into_owned(),
 763                )]));
 764            }
 765            Ok(variables)
 766        })
 767    }
 768
 769    fn associated_tasks(
 770        &self,
 771        file: Option<Arc<dyn language::File>>,
 772        cx: &App,
 773    ) -> Task<Option<TaskTemplates>> {
 774        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 775        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 776
 777        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 778        let package_to_run = language_sets
 779            .tasks
 780            .variables
 781            .get(DEFAULT_RUN_NAME_STR)
 782            .cloned();
 783        let custom_target_dir = language_sets
 784            .tasks
 785            .variables
 786            .get(CUSTOM_TARGET_DIR)
 787            .cloned();
 788        let run_task_args = if let Some(package_to_run) = package_to_run {
 789            vec!["run".into(), "-p".into(), package_to_run]
 790        } else {
 791            vec!["run".into()]
 792        };
 793        let mut task_templates = vec![
 794            TaskTemplate {
 795                label: format!(
 796                    "Check (package: {})",
 797                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 798                ),
 799                command: "cargo".into(),
 800                args: vec![
 801                    "check".into(),
 802                    "-p".into(),
 803                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 804                ],
 805                cwd: Some("$ZED_DIRNAME".to_owned()),
 806                ..TaskTemplate::default()
 807            },
 808            TaskTemplate {
 809                label: "Check all targets (workspace)".into(),
 810                command: "cargo".into(),
 811                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 812                cwd: Some("$ZED_DIRNAME".to_owned()),
 813                ..TaskTemplate::default()
 814            },
 815            TaskTemplate {
 816                label: format!(
 817                    "Test '{}' (package: {})",
 818                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 819                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 820                ),
 821                command: "cargo".into(),
 822                args: vec![
 823                    "test".into(),
 824                    "-p".into(),
 825                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 826                    "--".into(),
 827                    "--nocapture".into(),
 828                    "--include-ignored".into(),
 829                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 830                ],
 831                tags: vec!["rust-test".to_owned()],
 832                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 833                ..TaskTemplate::default()
 834            },
 835            TaskTemplate {
 836                label: format!(
 837                    "Doc test '{}' (package: {})",
 838                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 839                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 840                ),
 841                command: "cargo".into(),
 842                args: vec![
 843                    "test".into(),
 844                    "--doc".into(),
 845                    "-p".into(),
 846                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 847                    "--".into(),
 848                    "--nocapture".into(),
 849                    "--include-ignored".into(),
 850                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 851                ],
 852                tags: vec!["rust-doc-test".to_owned()],
 853                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 854                ..TaskTemplate::default()
 855            },
 856            TaskTemplate {
 857                label: format!(
 858                    "Test mod '{}' (package: {})",
 859                    VariableName::Stem.template_value(),
 860                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 861                ),
 862                command: "cargo".into(),
 863                args: vec![
 864                    "test".into(),
 865                    "-p".into(),
 866                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 867                    "--".into(),
 868                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 869                ],
 870                tags: vec!["rust-mod-test".to_owned()],
 871                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 872                ..TaskTemplate::default()
 873            },
 874            TaskTemplate {
 875                label: format!(
 876                    "Run {} {} (package: {})",
 877                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 878                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 879                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 880                ),
 881                command: "cargo".into(),
 882                args: vec![
 883                    "run".into(),
 884                    "-p".into(),
 885                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 886                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 887                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 888                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
 889                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
 890                ],
 891                cwd: Some("$ZED_DIRNAME".to_owned()),
 892                tags: vec!["rust-main".to_owned()],
 893                ..TaskTemplate::default()
 894            },
 895            TaskTemplate {
 896                label: format!(
 897                    "Test (package: {})",
 898                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 899                ),
 900                command: "cargo".into(),
 901                args: vec![
 902                    "test".into(),
 903                    "-p".into(),
 904                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 905                ],
 906                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 907                ..TaskTemplate::default()
 908            },
 909            TaskTemplate {
 910                label: "Run".into(),
 911                command: "cargo".into(),
 912                args: run_task_args,
 913                cwd: Some("$ZED_DIRNAME".to_owned()),
 914                ..TaskTemplate::default()
 915            },
 916            TaskTemplate {
 917                label: "Clean".into(),
 918                command: "cargo".into(),
 919                args: vec!["clean".into()],
 920                cwd: Some("$ZED_DIRNAME".to_owned()),
 921                ..TaskTemplate::default()
 922            },
 923        ];
 924
 925        if let Some(custom_target_dir) = custom_target_dir {
 926            task_templates = task_templates
 927                .into_iter()
 928                .map(|mut task_template| {
 929                    let mut args = task_template.args.split_off(1);
 930                    task_template.args.append(&mut vec![
 931                        "--target-dir".to_string(),
 932                        custom_target_dir.clone(),
 933                    ]);
 934                    task_template.args.append(&mut args);
 935
 936                    task_template
 937                })
 938                .collect();
 939        }
 940
 941        Task::ready(Some(TaskTemplates(task_templates)))
 942    }
 943
 944    fn lsp_task_source(&self) -> Option<LanguageServerName> {
 945        Some(SERVER_NAME)
 946    }
 947}
 948
 949/// Part of the data structure of Cargo metadata
 950#[derive(Debug, serde::Deserialize)]
 951struct CargoMetadata {
 952    packages: Vec<CargoPackage>,
 953}
 954
 955#[derive(Debug, serde::Deserialize)]
 956struct CargoPackage {
 957    id: String,
 958    targets: Vec<CargoTarget>,
 959    manifest_path: Arc<Path>,
 960}
 961
 962#[derive(Debug, serde::Deserialize)]
 963struct CargoTarget {
 964    name: String,
 965    kind: Vec<String>,
 966    src_path: String,
 967    #[serde(rename = "required-features", default)]
 968    required_features: Vec<String>,
 969}
 970
 971#[derive(Debug, PartialEq)]
 972enum TargetKind {
 973    Bin,
 974    Example,
 975}
 976
 977impl Display for TargetKind {
 978    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 979        match self {
 980            TargetKind::Bin => write!(f, "bin"),
 981            TargetKind::Example => write!(f, "example"),
 982        }
 983    }
 984}
 985
 986impl TryFrom<&str> for TargetKind {
 987    type Error = ();
 988    fn try_from(value: &str) -> Result<Self, ()> {
 989        match value {
 990            "bin" => Ok(Self::Bin),
 991            "example" => Ok(Self::Example),
 992            _ => Err(()),
 993        }
 994    }
 995}
 996/// Which package and binary target are we in?
 997#[derive(Debug, PartialEq)]
 998struct TargetInfo {
 999    package_name: String,
1000    target_name: String,
1001    target_kind: TargetKind,
1002    required_features: Vec<String>,
1003}
1004
1005async fn target_info_from_abs_path(
1006    abs_path: &Path,
1007    project_env: Option<&HashMap<String, String>>,
1008) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1009    let mut command = util::command::new_smol_command("cargo");
1010    if let Some(envs) = project_env {
1011        command.envs(envs);
1012    }
1013    let output = command
1014        .current_dir(abs_path.parent()?)
1015        .arg("metadata")
1016        .arg("--no-deps")
1017        .arg("--format-version")
1018        .arg("1")
1019        .output()
1020        .await
1021        .log_err()?
1022        .stdout;
1023
1024    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1025    target_info_from_metadata(metadata, abs_path)
1026}
1027
1028fn target_info_from_metadata(
1029    metadata: CargoMetadata,
1030    abs_path: &Path,
1031) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1032    let mut manifest_path = None;
1033    for package in metadata.packages {
1034        let Some(manifest_dir_path) = package.manifest_path.parent() else {
1035            continue;
1036        };
1037
1038        let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1039            continue;
1040        };
1041        let candidate_path_length = path_from_manifest_dir.components().count();
1042        // Pick the most specific manifest path
1043        if let Some((path, current_length)) = &mut manifest_path {
1044            if candidate_path_length > *current_length {
1045                *path = Arc::from(manifest_dir_path);
1046                *current_length = candidate_path_length;
1047            }
1048        } else {
1049            manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1050        };
1051
1052        for target in package.targets {
1053            let Some(bin_kind) = target
1054                .kind
1055                .iter()
1056                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1057            else {
1058                continue;
1059            };
1060            let target_path = PathBuf::from(target.src_path);
1061            if target_path == abs_path {
1062                return manifest_path.map(|(path, _)| {
1063                    (
1064                        package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1065                            package_name: package_name.to_owned(),
1066                            target_name: target.name,
1067                            required_features: target.required_features,
1068                            target_kind: bin_kind,
1069                        }),
1070                        path,
1071                    )
1072                });
1073            }
1074        }
1075    }
1076
1077    manifest_path.map(|(path, _)| (None, path))
1078}
1079
1080async fn human_readable_package_name(
1081    package_directory: &Path,
1082    project_env: Option<&HashMap<String, String>>,
1083) -> Option<String> {
1084    let mut command = util::command::new_smol_command("cargo");
1085    if let Some(envs) = project_env {
1086        command.envs(envs);
1087    }
1088    let pkgid = String::from_utf8(
1089        command
1090            .current_dir(package_directory)
1091            .arg("pkgid")
1092            .output()
1093            .await
1094            .log_err()?
1095            .stdout,
1096    )
1097    .ok()?;
1098    Some(package_name_from_pkgid(&pkgid)?.to_owned())
1099}
1100
1101// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1102// Output example in the root of Zed project:
1103// ```sh
1104// ❯ cargo pkgid zed
1105// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1106// ```
1107// Another variant, if a project has a custom package name or hyphen in the name:
1108// ```
1109// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1110// ```
1111//
1112// Extracts the package name from the output according to the spec:
1113// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1114fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1115    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1116        match input.rsplit_once(suffix_start) {
1117            Some((without_suffix, _)) => without_suffix,
1118            None => input,
1119        }
1120    }
1121
1122    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1123    let package_name = match version_suffix.rsplit_once('@') {
1124        Some((custom_package_name, _version)) => custom_package_name,
1125        None => {
1126            let host_and_path = split_off_suffix(version_prefix, '?');
1127            let (_, package_name) = host_and_path.rsplit_once('/')?;
1128            package_name
1129        }
1130    };
1131    Some(package_name)
1132}
1133
1134async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1135    maybe!(async {
1136        let mut last = None;
1137        let mut entries = fs::read_dir(&container_dir).await?;
1138        while let Some(entry) = entries.next().await {
1139            let path = entry?.path();
1140            if path.extension().is_some_and(|ext| ext == "metadata") {
1141                continue;
1142            }
1143            last = Some(path);
1144        }
1145
1146        let path = last.context("no cached binary")?;
1147        let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1148            AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1149            AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1150        };
1151
1152        anyhow::Ok(LanguageServerBinary {
1153            path,
1154            env: None,
1155            arguments: Default::default(),
1156        })
1157    })
1158    .await
1159    .log_err()
1160}
1161
1162fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1163    let fragment = if stem == "lib" {
1164        // This isn't quite right---it runs the tests for the entire library, rather than
1165        // just for the top-level `mod tests`. But we don't really have the means here to
1166        // filter out just that module.
1167        Some("--lib".to_owned())
1168    } else if stem == "mod" {
1169        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1170    } else if stem == "main" {
1171        if let (Some(bin_name), Some(bin_kind)) = (
1172            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1173            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1174        ) {
1175            Some(format!("--{bin_kind}={bin_name}"))
1176        } else {
1177            None
1178        }
1179    } else {
1180        Some(stem.to_owned())
1181    };
1182    fragment.unwrap_or_else(|| "--".to_owned())
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use std::num::NonZeroU32;
1188
1189    use super::*;
1190    use crate::language;
1191    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1192    use lsp::CompletionItemLabelDetails;
1193    use settings::SettingsStore;
1194    use theme::SyntaxTheme;
1195    use util::path;
1196
1197    #[gpui::test]
1198    async fn test_process_rust_diagnostics() {
1199        let mut params = lsp::PublishDiagnosticsParams {
1200            uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1201            version: None,
1202            diagnostics: vec![
1203                // no newlines
1204                lsp::Diagnostic {
1205                    message: "use of moved value `a`".to_string(),
1206                    ..Default::default()
1207                },
1208                // newline at the end of a code span
1209                lsp::Diagnostic {
1210                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1211                    ..Default::default()
1212                },
1213                // code span starting right after a newline
1214                lsp::Diagnostic {
1215                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1216                        .to_string(),
1217                    ..Default::default()
1218                },
1219            ],
1220        };
1221        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1222
1223        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1224
1225        // remove trailing newline from code span
1226        assert_eq!(
1227            params.diagnostics[1].message,
1228            "consider importing this struct: `use b::c;`"
1229        );
1230
1231        // do not remove newline before the start of code span
1232        assert_eq!(
1233            params.diagnostics[2].message,
1234            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1235        );
1236    }
1237
1238    #[gpui::test]
1239    async fn test_rust_label_for_completion() {
1240        let adapter = Arc::new(RustLspAdapter);
1241        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1242        let grammar = language.grammar().unwrap();
1243        let theme = SyntaxTheme::new_test([
1244            ("type", Hsla::default()),
1245            ("keyword", Hsla::default()),
1246            ("function", Hsla::default()),
1247            ("property", Hsla::default()),
1248        ]);
1249
1250        language.set_theme(&theme);
1251
1252        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1253        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1254        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1255        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1256
1257        assert_eq!(
1258            adapter
1259                .label_for_completion(
1260                    &lsp::CompletionItem {
1261                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1262                        label: "hello(…)".to_string(),
1263                        label_details: Some(CompletionItemLabelDetails {
1264                            detail: Some("(use crate::foo)".into()),
1265                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1266                        }),
1267                        ..Default::default()
1268                    },
1269                    &language
1270                )
1271                .await,
1272            Some(CodeLabel::new(
1273                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1274                0..5,
1275                vec![
1276                    (0..5, highlight_function),
1277                    (7..10, highlight_keyword),
1278                    (11..17, highlight_type),
1279                    (18..19, highlight_type),
1280                    (25..28, highlight_type),
1281                    (29..30, highlight_type),
1282                ],
1283            ))
1284        );
1285        assert_eq!(
1286            adapter
1287                .label_for_completion(
1288                    &lsp::CompletionItem {
1289                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1290                        label: "hello(…)".to_string(),
1291                        label_details: Some(CompletionItemLabelDetails {
1292                            detail: Some("(use crate::foo)".into()),
1293                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1294                        }),
1295                        ..Default::default()
1296                    },
1297                    &language
1298                )
1299                .await,
1300            Some(CodeLabel::new(
1301                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1302                0..5,
1303                vec![
1304                    (0..5, highlight_function),
1305                    (7..10, highlight_keyword),
1306                    (11..17, highlight_type),
1307                    (18..19, highlight_type),
1308                    (25..28, highlight_type),
1309                    (29..30, highlight_type),
1310                ],
1311            ))
1312        );
1313        assert_eq!(
1314            adapter
1315                .label_for_completion(
1316                    &lsp::CompletionItem {
1317                        kind: Some(lsp::CompletionItemKind::FIELD),
1318                        label: "len".to_string(),
1319                        detail: Some("usize".to_string()),
1320                        ..Default::default()
1321                    },
1322                    &language
1323                )
1324                .await,
1325            Some(CodeLabel::new(
1326                "len: usize".to_string(),
1327                0..3,
1328                vec![(0..3, highlight_field), (5..10, highlight_type),],
1329            ))
1330        );
1331
1332        assert_eq!(
1333            adapter
1334                .label_for_completion(
1335                    &lsp::CompletionItem {
1336                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1337                        label: "hello(…)".to_string(),
1338                        label_details: Some(CompletionItemLabelDetails {
1339                            detail: Some("(use crate::foo)".to_string()),
1340                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1341                        }),
1342
1343                        ..Default::default()
1344                    },
1345                    &language
1346                )
1347                .await,
1348            Some(CodeLabel::new(
1349                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1350                0..5,
1351                vec![
1352                    (0..5, highlight_function),
1353                    (7..10, highlight_keyword),
1354                    (11..17, highlight_type),
1355                    (18..19, highlight_type),
1356                    (25..28, highlight_type),
1357                    (29..30, highlight_type),
1358                ],
1359            ))
1360        );
1361
1362        assert_eq!(
1363            adapter
1364                .label_for_completion(
1365                    &lsp::CompletionItem {
1366                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1367                        label: "hello".to_string(),
1368                        label_details: Some(CompletionItemLabelDetails {
1369                            detail: Some("(use crate::foo)".to_string()),
1370                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1371                        }),
1372                        ..Default::default()
1373                    },
1374                    &language
1375                )
1376                .await,
1377            Some(CodeLabel::new(
1378                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1379                0..5,
1380                vec![
1381                    (0..5, highlight_function),
1382                    (7..10, highlight_keyword),
1383                    (11..17, highlight_type),
1384                    (18..19, highlight_type),
1385                    (25..28, highlight_type),
1386                    (29..30, highlight_type),
1387                ],
1388            ))
1389        );
1390
1391        assert_eq!(
1392            adapter
1393                .label_for_completion(
1394                    &lsp::CompletionItem {
1395                        kind: Some(lsp::CompletionItemKind::METHOD),
1396                        label: "await.as_deref_mut()".to_string(),
1397                        filter_text: Some("as_deref_mut".to_string()),
1398                        label_details: Some(CompletionItemLabelDetails {
1399                            detail: None,
1400                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1401                        }),
1402                        ..Default::default()
1403                    },
1404                    &language
1405                )
1406                .await,
1407            Some(CodeLabel::new(
1408                "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1409                6..18,
1410                vec![
1411                    (6..18, HighlightId(2)),
1412                    (20..23, HighlightId(1)),
1413                    (33..40, HighlightId(0)),
1414                    (45..46, HighlightId(0))
1415                ],
1416            ))
1417        );
1418
1419        assert_eq!(
1420            adapter
1421                .label_for_completion(
1422                    &lsp::CompletionItem {
1423                        kind: Some(lsp::CompletionItemKind::METHOD),
1424                        label: "as_deref_mut()".to_string(),
1425                        filter_text: Some("as_deref_mut".to_string()),
1426                        label_details: Some(CompletionItemLabelDetails {
1427                            detail: None,
1428                            description: Some(
1429                                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1430                            ),
1431                        }),
1432                        ..Default::default()
1433                    },
1434                    &language
1435                )
1436                .await,
1437            Some(CodeLabel::new(
1438                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1439                7..19,
1440                vec![
1441                    (0..3, HighlightId(1)),
1442                    (4..6, HighlightId(1)),
1443                    (7..19, HighlightId(2)),
1444                    (21..24, HighlightId(1)),
1445                    (34..41, HighlightId(0)),
1446                    (46..47, HighlightId(0))
1447                ],
1448            ))
1449        );
1450
1451        assert_eq!(
1452            adapter
1453                .label_for_completion(
1454                    &lsp::CompletionItem {
1455                        kind: Some(lsp::CompletionItemKind::FIELD),
1456                        label: "inner_value".to_string(),
1457                        filter_text: Some("value".to_string()),
1458                        detail: Some("String".to_string()),
1459                        ..Default::default()
1460                    },
1461                    &language,
1462                )
1463                .await,
1464            Some(CodeLabel::new(
1465                "inner_value: String".to_string(),
1466                6..11,
1467                vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1468            ))
1469        );
1470
1471        // Snippet with insert tabstop (empty placeholder)
1472        assert_eq!(
1473            adapter
1474                .label_for_completion(
1475                    &lsp::CompletionItem {
1476                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1477                        label: "println!".to_string(),
1478                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1479                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1480                            range: lsp::Range::default(),
1481                            new_text: "println!(\"$1\", $2)$0".to_string(),
1482                        })),
1483                        ..Default::default()
1484                    },
1485                    &language,
1486                )
1487                .await,
1488            Some(CodeLabel::new(
1489                "println!(\"\", …)".to_string(),
1490                0..8,
1491                vec![
1492                    (10..13, HighlightId::TABSTOP_INSERT_ID),
1493                    (16..19, HighlightId::TABSTOP_INSERT_ID),
1494                    (0..7, HighlightId(2)),
1495                    (7..8, HighlightId(2)),
1496                ],
1497            ))
1498        );
1499
1500        // Snippet with replace tabstop (placeholder with default text)
1501        assert_eq!(
1502            adapter
1503                .label_for_completion(
1504                    &lsp::CompletionItem {
1505                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1506                        label: "vec!".to_string(),
1507                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1508                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1509                            range: lsp::Range::default(),
1510                            new_text: "vec![${1:elem}]$0".to_string(),
1511                        })),
1512                        ..Default::default()
1513                    },
1514                    &language,
1515                )
1516                .await,
1517            Some(CodeLabel::new(
1518                "vec![elem]".to_string(),
1519                0..4,
1520                vec![
1521                    (5..9, HighlightId::TABSTOP_REPLACE_ID),
1522                    (0..3, HighlightId(2)),
1523                    (3..4, HighlightId(2)),
1524                ],
1525            ))
1526        );
1527
1528        // Snippet with tabstop appearing more than once
1529        assert_eq!(
1530            adapter
1531                .label_for_completion(
1532                    &lsp::CompletionItem {
1533                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1534                        label: "if let".to_string(),
1535                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1536                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1537                            range: lsp::Range::default(),
1538                            new_text: "if let ${1:pat} = $1 {\n    $0\n}".to_string(),
1539                        })),
1540                        ..Default::default()
1541                    },
1542                    &language,
1543                )
1544                .await,
1545            Some(CodeLabel::new(
1546                "if let pat = … {\n    \n}".to_string(),
1547                0..6,
1548                vec![
1549                    (7..10, HighlightId::TABSTOP_REPLACE_ID),
1550                    (13..16, HighlightId::TABSTOP_INSERT_ID),
1551                    (0..2, HighlightId(1)),
1552                    (3..6, HighlightId(1)),
1553                ],
1554            ))
1555        );
1556
1557        // Snippet with tabstops not in left-to-right order
1558        assert_eq!(
1559            adapter
1560                .label_for_completion(
1561                    &lsp::CompletionItem {
1562                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1563                        label: "for".to_string(),
1564                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1565                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1566                            range: lsp::Range::default(),
1567                            new_text: "for ${2:item} in ${1:iter} {\n    $0\n}".to_string(),
1568                        })),
1569                        ..Default::default()
1570                    },
1571                    &language,
1572                )
1573                .await,
1574            Some(CodeLabel::new(
1575                "for item in iter {\n    \n}".to_string(),
1576                0..3,
1577                vec![
1578                    (4..8, HighlightId::TABSTOP_REPLACE_ID),
1579                    (12..16, HighlightId::TABSTOP_REPLACE_ID),
1580                    (0..3, HighlightId(1)),
1581                    (9..11, HighlightId(1)),
1582                ],
1583            ))
1584        );
1585
1586        // Postfix completion without actual tabstops (only implicit final $0)
1587        // The label should use completion.label so it can be filtered by "ref"
1588        let ref_completion = adapter
1589            .label_for_completion(
1590                &lsp::CompletionItem {
1591                    kind: Some(lsp::CompletionItemKind::SNIPPET),
1592                    label: "ref".to_string(),
1593                    filter_text: Some("ref".to_string()),
1594                    label_details: Some(CompletionItemLabelDetails {
1595                        detail: None,
1596                        description: Some("&expr".to_string()),
1597                    }),
1598                    detail: Some("&expr".to_string()),
1599                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1600                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1601                        range: lsp::Range::default(),
1602                        new_text: "&String::new()".to_string(),
1603                    })),
1604                    ..Default::default()
1605                },
1606                &language,
1607            )
1608            .await;
1609        assert!(
1610            ref_completion.is_some(),
1611            "ref postfix completion should have a label"
1612        );
1613        let ref_label = ref_completion.unwrap();
1614        let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1615        assert!(
1616            filter_text.contains("ref"),
1617            "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1618        );
1619
1620        // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1621        let res = adapter
1622            .label_for_completion(
1623                &lsp::CompletionItem {
1624                    kind: Some(lsp::CompletionItemKind::STRUCT),
1625                    label: "Particles".to_string(),
1626                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1627                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1628                        range: lsp::Range::default(),
1629                        new_text: "Particles { pos_x: $1, pos_y: $2, vel_x: $3, vel_y: $4, acc_x: ${5:()}, acc_y: ${6:()}, mass: $7 }$0".to_string(),
1630                    })),
1631                    ..Default::default()
1632                },
1633                &language,
1634            )
1635            .await
1636            .unwrap();
1637
1638        assert_eq!(
1639            res,
1640            CodeLabel::new(
1641                "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1642                0..9,
1643                vec![
1644                    (19..22, HighlightId::TABSTOP_INSERT_ID),
1645                    (31..34, HighlightId::TABSTOP_INSERT_ID),
1646                    (43..46, HighlightId::TABSTOP_INSERT_ID),
1647                    (55..58, HighlightId::TABSTOP_INSERT_ID),
1648                    (67..69, HighlightId::TABSTOP_REPLACE_ID),
1649                    (78..80, HighlightId::TABSTOP_REPLACE_ID),
1650                    (88..91, HighlightId::TABSTOP_INSERT_ID),
1651                    (0..9, highlight_type),
1652                    (60..65, highlight_field),
1653                    (71..76, highlight_field),
1654                ],
1655            )
1656        );
1657    }
1658
1659    #[gpui::test]
1660    async fn test_rust_label_for_symbol() {
1661        let adapter = Arc::new(RustLspAdapter);
1662        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1663        let grammar = language.grammar().unwrap();
1664        let theme = SyntaxTheme::new_test([
1665            ("type", Hsla::default()),
1666            ("keyword", Hsla::default()),
1667            ("function", Hsla::default()),
1668            ("property", Hsla::default()),
1669        ]);
1670
1671        language.set_theme(&theme);
1672
1673        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1674        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1675        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1676
1677        assert_eq!(
1678            adapter
1679                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1680                .await,
1681            Some(CodeLabel::new(
1682                "fn hello".to_string(),
1683                3..8,
1684                vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1685            ))
1686        );
1687
1688        assert_eq!(
1689            adapter
1690                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1691                .await,
1692            Some(CodeLabel::new(
1693                "type World".to_string(),
1694                5..10,
1695                vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1696            ))
1697        );
1698    }
1699
1700    #[gpui::test]
1701    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1702        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1703        cx.update(|cx| {
1704            let test_settings = SettingsStore::test(cx);
1705            cx.set_global(test_settings);
1706            cx.update_global::<SettingsStore, _>(|store, cx| {
1707                store.update_user_settings(cx, |s| {
1708                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1709                });
1710            });
1711        });
1712
1713        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1714
1715        cx.new(|cx| {
1716            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1717
1718            // indent between braces
1719            buffer.set_text("fn a() {}", cx);
1720            let ix = buffer.len() - 1;
1721            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1722            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1723
1724            // indent between braces, even after empty lines
1725            buffer.set_text("fn a() {\n\n\n}", cx);
1726            let ix = buffer.len() - 2;
1727            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1728            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1729
1730            // indent a line that continues a field expression
1731            buffer.set_text("fn a() {\n  \n}", cx);
1732            let ix = buffer.len() - 2;
1733            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1734            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1735
1736            // indent further lines that continue the field expression, even after empty lines
1737            let ix = buffer.len() - 2;
1738            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1739            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1740
1741            // dedent the line after the field expression
1742            let ix = buffer.len() - 2;
1743            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1744            assert_eq!(
1745                buffer.text(),
1746                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1747            );
1748
1749            // indent inside a struct within a call
1750            buffer.set_text("const a: B = c(D {});", cx);
1751            let ix = buffer.len() - 3;
1752            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1753            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1754
1755            // indent further inside a nested call
1756            let ix = buffer.len() - 4;
1757            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1758            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1759
1760            // keep that indent after an empty line
1761            let ix = buffer.len() - 8;
1762            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1763            assert_eq!(
1764                buffer.text(),
1765                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1766            );
1767
1768            buffer
1769        });
1770    }
1771
1772    #[test]
1773    fn test_package_name_from_pkgid() {
1774        for (input, expected) in [
1775            (
1776                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1777                "zed",
1778            ),
1779            (
1780                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1781                "my-custom-package",
1782            ),
1783        ] {
1784            assert_eq!(package_name_from_pkgid(input), Some(expected));
1785        }
1786    }
1787
1788    #[test]
1789    fn test_target_info_from_metadata() {
1790        for (input, absolute_path, expected) in [
1791            (
1792                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"}]}]}"#,
1793                "/path/to/zed/src/main.rs",
1794                Some((
1795                    Some(TargetInfo {
1796                        package_name: "zed".into(),
1797                        target_name: "zed".into(),
1798                        required_features: Vec::new(),
1799                        target_kind: TargetKind::Bin,
1800                    }),
1801                    Arc::from("/path/to/zed".as_ref()),
1802                )),
1803            ),
1804            (
1805                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"}]}]}"#,
1806                "/path/to/custom-package/src/main.rs",
1807                Some((
1808                    Some(TargetInfo {
1809                        package_name: "my-custom-package".into(),
1810                        target_name: "my-custom-bin".into(),
1811                        required_features: Vec::new(),
1812                        target_kind: TargetKind::Bin,
1813                    }),
1814                    Arc::from("/path/to/custom-package".as_ref()),
1815                )),
1816            ),
1817            (
1818                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"}]}"#,
1819                "/path/to/custom-package/src/main.rs",
1820                Some((
1821                    Some(TargetInfo {
1822                        package_name: "my-custom-package".into(),
1823                        target_name: "my-custom-bin".into(),
1824                        required_features: Vec::new(),
1825                        target_kind: TargetKind::Example,
1826                    }),
1827                    Arc::from("/path/to/custom-package".as_ref()),
1828                )),
1829            ),
1830            (
1831                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"]}]}]}"#,
1832                "/path/to/custom-package/src/main.rs",
1833                Some((
1834                    Some(TargetInfo {
1835                        package_name: "my-custom-package".into(),
1836                        target_name: "my-custom-bin".into(),
1837                        required_features: vec!["foo".to_owned(), "bar".to_owned()],
1838                        target_kind: TargetKind::Example,
1839                    }),
1840                    Arc::from("/path/to/custom-package".as_ref()),
1841                )),
1842            ),
1843            (
1844                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"}]}"#,
1845                "/path/to/custom-package/src/main.rs",
1846                Some((
1847                    Some(TargetInfo {
1848                        package_name: "my-custom-package".into(),
1849                        target_name: "my-custom-bin".into(),
1850                        required_features: vec![],
1851                        target_kind: TargetKind::Example,
1852                    }),
1853                    Arc::from("/path/to/custom-package".as_ref()),
1854                )),
1855            ),
1856            (
1857                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"}]}"#,
1858                "/path/to/custom-package/src/main.rs",
1859                Some((None, Arc::from("/path/to/custom-package".as_ref()))),
1860            ),
1861        ] {
1862            let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
1863
1864            let absolute_path = Path::new(absolute_path);
1865
1866            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1867        }
1868    }
1869
1870    #[test]
1871    fn test_rust_test_fragment() {
1872        #[track_caller]
1873        fn check(
1874            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1875            path: &str,
1876            expected: &str,
1877        ) {
1878            let path = Path::new(path);
1879            let found = test_fragment(
1880                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1881                path,
1882                path.file_stem().unwrap().to_str().unwrap(),
1883            );
1884            assert_eq!(expected, found);
1885        }
1886
1887        check([], "/project/src/lib.rs", "--lib");
1888        check([], "/project/src/foo/mod.rs", "foo");
1889        check(
1890            [
1891                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1892                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1893            ],
1894            "/project/src/main.rs",
1895            "--bin=x",
1896        );
1897        check([], "/project/src/main.rs", "--");
1898    }
1899}