rust.rs

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