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, 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::language_settings;
  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        file: Option<Arc<dyn language::File>>,
 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_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 908        let package_to_run = language_sets
 909            .tasks
 910            .variables
 911            .get(DEFAULT_RUN_NAME_STR)
 912            .cloned();
 913        let custom_target_dir = language_sets
 914            .tasks
 915            .variables
 916            .get(CUSTOM_TARGET_DIR)
 917            .cloned();
 918        let run_task_args = if let Some(package_to_run) = package_to_run {
 919            vec!["run".into(), "-p".into(), package_to_run]
 920        } else {
 921            vec!["run".into()]
 922        };
 923        let mut task_templates = vec![
 924            TaskTemplate {
 925                label: format!(
 926                    "Check (package: {})",
 927                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 928                ),
 929                command: "cargo".into(),
 930                args: vec![
 931                    "check".into(),
 932                    "-p".into(),
 933                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 934                ],
 935                cwd: Some("$ZED_DIRNAME".to_owned()),
 936                ..TaskTemplate::default()
 937            },
 938            TaskTemplate {
 939                label: "Check all targets (workspace)".into(),
 940                command: "cargo".into(),
 941                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 942                cwd: Some("$ZED_DIRNAME".to_owned()),
 943                ..TaskTemplate::default()
 944            },
 945            TaskTemplate {
 946                label: format!(
 947                    "Test '{}' (package: {})",
 948                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 949                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 950                ),
 951                command: "cargo".into(),
 952                args: vec![
 953                    "test".into(),
 954                    "-p".into(),
 955                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 956                    "--".into(),
 957                    "--nocapture".into(),
 958                    "--include-ignored".into(),
 959                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 960                ],
 961                tags: vec!["rust-test".to_owned()],
 962                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 963                ..TaskTemplate::default()
 964            },
 965            TaskTemplate {
 966                label: format!(
 967                    "Doc test '{}' (package: {})",
 968                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 969                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 970                ),
 971                command: "cargo".into(),
 972                args: vec![
 973                    "test".into(),
 974                    "--doc".into(),
 975                    "-p".into(),
 976                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 977                    "--".into(),
 978                    "--nocapture".into(),
 979                    "--include-ignored".into(),
 980                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 981                ],
 982                tags: vec!["rust-doc-test".to_owned()],
 983                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
 984                ..TaskTemplate::default()
 985            },
 986            TaskTemplate {
 987                label: format!(
 988                    "Test mod '{}' (package: {})",
 989                    VariableName::Stem.template_value(),
 990                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 991                ),
 992                command: "cargo".into(),
 993                args: vec![
 994                    "test".into(),
 995                    "-p".into(),
 996                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 997                    "--".into(),
 998                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 999                ],
1000                tags: vec!["rust-mod-test".to_owned()],
1001                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1002                ..TaskTemplate::default()
1003            },
1004            TaskTemplate {
1005                label: format!(
1006                    "Run {} {} (package: {})",
1007                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
1008                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1009                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
1010                ),
1011                command: "cargo".into(),
1012                args: vec![
1013                    "run".into(),
1014                    "-p".into(),
1015                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
1016                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
1017                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
1018                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
1019                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
1020                ],
1021                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1022                tags: vec!["rust-main".to_owned()],
1023                ..TaskTemplate::default()
1024            },
1025            TaskTemplate {
1026                label: format!(
1027                    "Test (package: {})",
1028                    RUST_PACKAGE_TASK_VARIABLE.template_value()
1029                ),
1030                command: "cargo".into(),
1031                args: vec![
1032                    "test".into(),
1033                    "-p".into(),
1034                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
1035                ],
1036                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1037                ..TaskTemplate::default()
1038            },
1039            TaskTemplate {
1040                label: "Run".into(),
1041                command: "cargo".into(),
1042                args: run_task_args,
1043                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1044                ..TaskTemplate::default()
1045            },
1046            TaskTemplate {
1047                label: "Clean".into(),
1048                command: "cargo".into(),
1049                args: vec!["clean".into()],
1050                cwd: Some(RUST_MANIFEST_DIRNAME_TASK_VARIABLE.template_value()),
1051                ..TaskTemplate::default()
1052            },
1053        ];
1054
1055        if let Some(custom_target_dir) = custom_target_dir {
1056            task_templates = task_templates
1057                .into_iter()
1058                .map(|mut task_template| {
1059                    let mut args = task_template.args.split_off(1);
1060                    task_template.args.append(&mut vec![
1061                        "--target-dir".to_string(),
1062                        custom_target_dir.clone(),
1063                    ]);
1064                    task_template.args.append(&mut args);
1065
1066                    task_template
1067                })
1068                .collect();
1069        }
1070
1071        Task::ready(Some(TaskTemplates(task_templates)))
1072    }
1073
1074    fn lsp_task_source(&self) -> Option<LanguageServerName> {
1075        Some(SERVER_NAME)
1076    }
1077}
1078
1079/// Part of the data structure of Cargo metadata
1080#[derive(Debug, serde::Deserialize)]
1081struct CargoMetadata {
1082    packages: Vec<CargoPackage>,
1083}
1084
1085#[derive(Debug, serde::Deserialize)]
1086struct CargoPackage {
1087    id: String,
1088    targets: Vec<CargoTarget>,
1089    manifest_path: Arc<Path>,
1090}
1091
1092#[derive(Debug, serde::Deserialize)]
1093struct CargoTarget {
1094    name: String,
1095    kind: Vec<String>,
1096    src_path: String,
1097    #[serde(rename = "required-features", default)]
1098    required_features: Vec<String>,
1099}
1100
1101#[derive(Debug, PartialEq)]
1102enum TargetKind {
1103    Bin,
1104    Example,
1105}
1106
1107impl Display for TargetKind {
1108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1109        match self {
1110            TargetKind::Bin => write!(f, "bin"),
1111            TargetKind::Example => write!(f, "example"),
1112        }
1113    }
1114}
1115
1116impl TryFrom<&str> for TargetKind {
1117    type Error = ();
1118    fn try_from(value: &str) -> Result<Self, ()> {
1119        match value {
1120            "bin" => Ok(Self::Bin),
1121            "example" => Ok(Self::Example),
1122            _ => Err(()),
1123        }
1124    }
1125}
1126/// Which package and binary target are we in?
1127#[derive(Debug, PartialEq)]
1128struct TargetInfo {
1129    package_name: String,
1130    target_name: String,
1131    target_kind: TargetKind,
1132    required_features: Vec<String>,
1133}
1134
1135async fn target_info_from_abs_path(
1136    abs_path: &Path,
1137    project_env: Option<&HashMap<String, String>>,
1138) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1139    let mut command = util::command::new_command("cargo");
1140    if let Some(envs) = project_env {
1141        command.envs(envs);
1142    }
1143    let output = command
1144        .current_dir(abs_path.parent()?)
1145        .arg("metadata")
1146        .arg("--no-deps")
1147        .arg("--format-version")
1148        .arg("1")
1149        .output()
1150        .await
1151        .log_err()?
1152        .stdout;
1153
1154    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
1155    target_info_from_metadata(metadata, abs_path)
1156}
1157
1158fn target_info_from_metadata(
1159    metadata: CargoMetadata,
1160    abs_path: &Path,
1161) -> Option<(Option<TargetInfo>, Arc<Path>)> {
1162    let mut manifest_path = None;
1163    for package in metadata.packages {
1164        let Some(manifest_dir_path) = package.manifest_path.parent() else {
1165            continue;
1166        };
1167
1168        let Some(path_from_manifest_dir) = abs_path.strip_prefix(manifest_dir_path).ok() else {
1169            continue;
1170        };
1171        let candidate_path_length = path_from_manifest_dir.components().count();
1172        // Pick the most specific manifest path
1173        if let Some((path, current_length)) = &mut manifest_path {
1174            if candidate_path_length > *current_length {
1175                *path = Arc::from(manifest_dir_path);
1176                *current_length = candidate_path_length;
1177            }
1178        } else {
1179            manifest_path = Some((Arc::from(manifest_dir_path), candidate_path_length));
1180        };
1181
1182        for target in package.targets {
1183            let Some(bin_kind) = target
1184                .kind
1185                .iter()
1186                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
1187            else {
1188                continue;
1189            };
1190            let target_path = PathBuf::from(target.src_path);
1191            if target_path == abs_path {
1192                return manifest_path.map(|(path, _)| {
1193                    (
1194                        package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
1195                            package_name: package_name.to_owned(),
1196                            target_name: target.name,
1197                            required_features: target.required_features,
1198                            target_kind: bin_kind,
1199                        }),
1200                        path,
1201                    )
1202                });
1203            }
1204        }
1205    }
1206
1207    manifest_path.map(|(path, _)| (None, path))
1208}
1209
1210async fn human_readable_package_name(
1211    package_directory: &Path,
1212    project_env: Option<&HashMap<String, String>>,
1213) -> Option<String> {
1214    let mut command = util::command::new_command("cargo");
1215    if let Some(envs) = project_env {
1216        command.envs(envs);
1217    }
1218    let pkgid = String::from_utf8(
1219        command
1220            .current_dir(package_directory)
1221            .arg("pkgid")
1222            .output()
1223            .await
1224            .log_err()?
1225            .stdout,
1226    )
1227    .ok()?;
1228    Some(package_name_from_pkgid(&pkgid)?.to_owned())
1229}
1230
1231// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
1232// Output example in the root of Zed project:
1233// ```sh
1234// ❯ cargo pkgid zed
1235// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
1236// ```
1237// Another variant, if a project has a custom package name or hyphen in the name:
1238// ```
1239// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
1240// ```
1241//
1242// Extracts the package name from the output according to the spec:
1243// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
1244fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
1245    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
1246        match input.rsplit_once(suffix_start) {
1247            Some((without_suffix, _)) => without_suffix,
1248            None => input,
1249        }
1250    }
1251
1252    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
1253    let package_name = match version_suffix.rsplit_once('@') {
1254        Some((custom_package_name, _version)) => custom_package_name,
1255        None => {
1256            let host_and_path = split_off_suffix(version_prefix, '?');
1257            let (_, package_name) = host_and_path.rsplit_once('/')?;
1258            package_name
1259        }
1260    };
1261    Some(package_name)
1262}
1263
1264async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
1265    let binary_result = maybe!(async {
1266        let mut last = None;
1267        let mut entries = fs::read_dir(&container_dir)
1268            .await
1269            .with_context(|| format!("listing {container_dir:?}"))?;
1270        while let Some(entry) = entries.next().await {
1271            let path = entry?.path();
1272            if path.extension().is_some_and(|ext| ext == "metadata") {
1273                continue;
1274            }
1275            last = Some(path);
1276        }
1277
1278        let path = match last {
1279            Some(last) => last,
1280            None => return Ok(None),
1281        };
1282        let path = match RustLspAdapter::GITHUB_ASSET_KIND {
1283            AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
1284            AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
1285        };
1286
1287        anyhow::Ok(Some(LanguageServerBinary {
1288            path,
1289            env: None,
1290            arguments: Vec::new(),
1291        }))
1292    })
1293    .await;
1294
1295    match binary_result {
1296        Ok(Some(binary)) => Some(binary),
1297        Ok(None) => {
1298            log::info!("No cached rust-analyzer binary found");
1299            None
1300        }
1301        Err(e) => {
1302            log::error!("Failed to look up cached rust-analyzer binary: {e:#}");
1303            None
1304        }
1305    }
1306}
1307
1308fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1309    let fragment = if stem == "lib" {
1310        // This isn't quite right---it runs the tests for the entire library, rather than
1311        // just for the top-level `mod tests`. But we don't really have the means here to
1312        // filter out just that module.
1313        Some("--lib".to_owned())
1314    } else if stem == "mod" {
1315        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().into_owned()) })
1316    } else if stem == "main" {
1317        if let (Some(bin_name), Some(bin_kind)) = (
1318            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1319            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1320        ) {
1321            Some(format!("--{bin_kind}={bin_name}"))
1322        } else {
1323            None
1324        }
1325    } else {
1326        Some(stem.to_owned())
1327    };
1328    fragment.unwrap_or_else(|| "--".to_owned())
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333    use std::num::NonZeroU32;
1334
1335    use super::*;
1336    use crate::language;
1337    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1338    use lsp::CompletionItemLabelDetails;
1339    use settings::SettingsStore;
1340    use theme::SyntaxTheme;
1341    use util::path;
1342
1343    #[gpui::test]
1344    async fn test_process_rust_diagnostics() {
1345        let mut params = lsp::PublishDiagnosticsParams {
1346            uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
1347            version: None,
1348            diagnostics: vec![
1349                // no newlines
1350                lsp::Diagnostic {
1351                    message: "use of moved value `a`".to_string(),
1352                    ..Default::default()
1353                },
1354                // newline at the end of a code span
1355                lsp::Diagnostic {
1356                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1357                    ..Default::default()
1358                },
1359                // code span starting right after a newline
1360                lsp::Diagnostic {
1361                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1362                        .to_string(),
1363                    ..Default::default()
1364                },
1365            ],
1366        };
1367        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1368
1369        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1370
1371        // remove trailing newline from code span
1372        assert_eq!(
1373            params.diagnostics[1].message,
1374            "consider importing this struct: `use b::c;`"
1375        );
1376
1377        // do not remove newline before the start of code span
1378        assert_eq!(
1379            params.diagnostics[2].message,
1380            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1381        );
1382    }
1383
1384    #[gpui::test]
1385    async fn test_rust_label_for_completion() {
1386        let adapter = Arc::new(RustLspAdapter);
1387        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1388        let grammar = language.grammar().unwrap();
1389        let theme = SyntaxTheme::new_test([
1390            ("type", Hsla::default()),
1391            ("keyword", Hsla::default()),
1392            ("function", Hsla::default()),
1393            ("property", Hsla::default()),
1394        ]);
1395
1396        language.set_theme(&theme);
1397
1398        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1399        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1400        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1401        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1402
1403        assert_eq!(
1404            adapter
1405                .label_for_completion(
1406                    &lsp::CompletionItem {
1407                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1408                        label: "hello(…)".to_string(),
1409                        label_details: Some(CompletionItemLabelDetails {
1410                            detail: Some("(use crate::foo)".into()),
1411                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1412                        }),
1413                        ..Default::default()
1414                    },
1415                    &language
1416                )
1417                .await,
1418            Some(CodeLabel::new(
1419                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1420                0..5,
1421                vec![
1422                    (0..5, highlight_function),
1423                    (7..10, highlight_keyword),
1424                    (11..17, highlight_type),
1425                    (18..19, highlight_type),
1426                    (25..28, highlight_type),
1427                    (29..30, highlight_type),
1428                ],
1429            ))
1430        );
1431        assert_eq!(
1432            adapter
1433                .label_for_completion(
1434                    &lsp::CompletionItem {
1435                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1436                        label: "hello(…)".to_string(),
1437                        label_details: Some(CompletionItemLabelDetails {
1438                            detail: Some("(use crate::foo)".into()),
1439                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1440                        }),
1441                        ..Default::default()
1442                    },
1443                    &language
1444                )
1445                .await,
1446            Some(CodeLabel::new(
1447                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1448                0..5,
1449                vec![
1450                    (0..5, highlight_function),
1451                    (7..10, highlight_keyword),
1452                    (11..17, highlight_type),
1453                    (18..19, highlight_type),
1454                    (25..28, highlight_type),
1455                    (29..30, highlight_type),
1456                ],
1457            ))
1458        );
1459        assert_eq!(
1460            adapter
1461                .label_for_completion(
1462                    &lsp::CompletionItem {
1463                        kind: Some(lsp::CompletionItemKind::FIELD),
1464                        label: "len".to_string(),
1465                        detail: Some("usize".to_string()),
1466                        ..Default::default()
1467                    },
1468                    &language
1469                )
1470                .await,
1471            Some(CodeLabel::new(
1472                "len: usize".to_string(),
1473                0..3,
1474                vec![(0..3, highlight_field), (5..10, highlight_type),],
1475            ))
1476        );
1477
1478        assert_eq!(
1479            adapter
1480                .label_for_completion(
1481                    &lsp::CompletionItem {
1482                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1483                        label: "hello(…)".to_string(),
1484                        label_details: Some(CompletionItemLabelDetails {
1485                            detail: Some("(use crate::foo)".to_string()),
1486                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1487                        }),
1488
1489                        ..Default::default()
1490                    },
1491                    &language
1492                )
1493                .await,
1494            Some(CodeLabel::new(
1495                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1496                0..5,
1497                vec![
1498                    (0..5, highlight_function),
1499                    (7..10, highlight_keyword),
1500                    (11..17, highlight_type),
1501                    (18..19, highlight_type),
1502                    (25..28, highlight_type),
1503                    (29..30, highlight_type),
1504                ],
1505            ))
1506        );
1507
1508        assert_eq!(
1509            adapter
1510                .label_for_completion(
1511                    &lsp::CompletionItem {
1512                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1513                        label: "hello".to_string(),
1514                        label_details: Some(CompletionItemLabelDetails {
1515                            detail: Some("(use crate::foo)".to_string()),
1516                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1517                        }),
1518                        ..Default::default()
1519                    },
1520                    &language
1521                )
1522                .await,
1523            Some(CodeLabel::new(
1524                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1525                0..5,
1526                vec![
1527                    (0..5, highlight_function),
1528                    (7..10, highlight_keyword),
1529                    (11..17, highlight_type),
1530                    (18..19, highlight_type),
1531                    (25..28, highlight_type),
1532                    (29..30, highlight_type),
1533                ],
1534            ))
1535        );
1536
1537        assert_eq!(
1538            adapter
1539                .label_for_completion(
1540                    &lsp::CompletionItem {
1541                        kind: Some(lsp::CompletionItemKind::METHOD),
1542                        label: "await.as_deref_mut()".to_string(),
1543                        filter_text: Some("as_deref_mut".to_string()),
1544                        label_details: Some(CompletionItemLabelDetails {
1545                            detail: None,
1546                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1547                        }),
1548                        ..Default::default()
1549                    },
1550                    &language
1551                )
1552                .await,
1553            Some(CodeLabel::new(
1554                "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1555                6..18,
1556                vec![
1557                    (6..18, HighlightId(2)),
1558                    (20..23, HighlightId(1)),
1559                    (33..40, HighlightId(0)),
1560                    (45..46, HighlightId(0))
1561                ],
1562            ))
1563        );
1564
1565        assert_eq!(
1566            adapter
1567                .label_for_completion(
1568                    &lsp::CompletionItem {
1569                        kind: Some(lsp::CompletionItemKind::METHOD),
1570                        label: "as_deref_mut()".to_string(),
1571                        filter_text: Some("as_deref_mut".to_string()),
1572                        label_details: Some(CompletionItemLabelDetails {
1573                            detail: None,
1574                            description: Some(
1575                                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string()
1576                            ),
1577                        }),
1578                        ..Default::default()
1579                    },
1580                    &language
1581                )
1582                .await,
1583            Some(CodeLabel::new(
1584                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
1585                7..19,
1586                vec![
1587                    (0..3, HighlightId(1)),
1588                    (4..6, HighlightId(1)),
1589                    (7..19, HighlightId(2)),
1590                    (21..24, HighlightId(1)),
1591                    (34..41, HighlightId(0)),
1592                    (46..47, HighlightId(0))
1593                ],
1594            ))
1595        );
1596
1597        assert_eq!(
1598            adapter
1599                .label_for_completion(
1600                    &lsp::CompletionItem {
1601                        kind: Some(lsp::CompletionItemKind::FIELD),
1602                        label: "inner_value".to_string(),
1603                        filter_text: Some("value".to_string()),
1604                        detail: Some("String".to_string()),
1605                        ..Default::default()
1606                    },
1607                    &language,
1608                )
1609                .await,
1610            Some(CodeLabel::new(
1611                "inner_value: String".to_string(),
1612                6..11,
1613                vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1614            ))
1615        );
1616
1617        // Snippet with insert tabstop (empty placeholder)
1618        assert_eq!(
1619            adapter
1620                .label_for_completion(
1621                    &lsp::CompletionItem {
1622                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1623                        label: "println!".to_string(),
1624                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1625                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1626                            range: lsp::Range::default(),
1627                            new_text: "println!(\"$1\", $2)$0".to_string(),
1628                        })),
1629                        ..Default::default()
1630                    },
1631                    &language,
1632                )
1633                .await,
1634            Some(CodeLabel::new(
1635                "println!(\"\", …)".to_string(),
1636                0..8,
1637                vec![
1638                    (10..13, HighlightId::TABSTOP_INSERT_ID),
1639                    (16..19, HighlightId::TABSTOP_INSERT_ID),
1640                    (0..7, HighlightId(2)),
1641                    (7..8, HighlightId(2)),
1642                ],
1643            ))
1644        );
1645
1646        // Snippet with replace tabstop (placeholder with default text)
1647        assert_eq!(
1648            adapter
1649                .label_for_completion(
1650                    &lsp::CompletionItem {
1651                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1652                        label: "vec!".to_string(),
1653                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1654                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1655                            range: lsp::Range::default(),
1656                            new_text: "vec![${1:elem}]$0".to_string(),
1657                        })),
1658                        ..Default::default()
1659                    },
1660                    &language,
1661                )
1662                .await,
1663            Some(CodeLabel::new(
1664                "vec![elem]".to_string(),
1665                0..4,
1666                vec![
1667                    (5..9, HighlightId::TABSTOP_REPLACE_ID),
1668                    (0..3, HighlightId(2)),
1669                    (3..4, HighlightId(2)),
1670                ],
1671            ))
1672        );
1673
1674        // Snippet with tabstop appearing more than once
1675        assert_eq!(
1676            adapter
1677                .label_for_completion(
1678                    &lsp::CompletionItem {
1679                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1680                        label: "if let".to_string(),
1681                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1682                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1683                            range: lsp::Range::default(),
1684                            new_text: "if let ${1:pat} = $1 {\n    $0\n}".to_string(),
1685                        })),
1686                        ..Default::default()
1687                    },
1688                    &language,
1689                )
1690                .await,
1691            Some(CodeLabel::new(
1692                "if let pat = … {\n    \n}".to_string(),
1693                0..6,
1694                vec![
1695                    (7..10, HighlightId::TABSTOP_REPLACE_ID),
1696                    (13..16, HighlightId::TABSTOP_INSERT_ID),
1697                    (0..2, HighlightId(1)),
1698                    (3..6, HighlightId(1)),
1699                ],
1700            ))
1701        );
1702
1703        // Snippet with tabstops not in left-to-right order
1704        assert_eq!(
1705            adapter
1706                .label_for_completion(
1707                    &lsp::CompletionItem {
1708                        kind: Some(lsp::CompletionItemKind::SNIPPET),
1709                        label: "for".to_string(),
1710                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1711                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1712                            range: lsp::Range::default(),
1713                            new_text: "for ${2:item} in ${1:iter} {\n    $0\n}".to_string(),
1714                        })),
1715                        ..Default::default()
1716                    },
1717                    &language,
1718                )
1719                .await,
1720            Some(CodeLabel::new(
1721                "for item in iter {\n    \n}".to_string(),
1722                0..3,
1723                vec![
1724                    (4..8, HighlightId::TABSTOP_REPLACE_ID),
1725                    (12..16, HighlightId::TABSTOP_REPLACE_ID),
1726                    (0..3, HighlightId(1)),
1727                    (9..11, HighlightId(1)),
1728                ],
1729            ))
1730        );
1731
1732        // Postfix completion without actual tabstops (only implicit final $0)
1733        // The label should use completion.label so it can be filtered by "ref"
1734        let ref_completion = adapter
1735            .label_for_completion(
1736                &lsp::CompletionItem {
1737                    kind: Some(lsp::CompletionItemKind::SNIPPET),
1738                    label: "ref".to_string(),
1739                    filter_text: Some("ref".to_string()),
1740                    label_details: Some(CompletionItemLabelDetails {
1741                        detail: None,
1742                        description: Some("&expr".to_string()),
1743                    }),
1744                    detail: Some("&expr".to_string()),
1745                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1746                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1747                        range: lsp::Range::default(),
1748                        new_text: "&String::new()".to_string(),
1749                    })),
1750                    ..Default::default()
1751                },
1752                &language,
1753            )
1754            .await;
1755        assert!(
1756            ref_completion.is_some(),
1757            "ref postfix completion should have a label"
1758        );
1759        let ref_label = ref_completion.unwrap();
1760        let filter_text = &ref_label.text[ref_label.filter_range.clone()];
1761        assert!(
1762            filter_text.contains("ref"),
1763            "filter range text '{filter_text}' should contain 'ref' for filtering to work",
1764        );
1765
1766        // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
1767        let res = adapter
1768            .label_for_completion(
1769                &lsp::CompletionItem {
1770                    kind: Some(lsp::CompletionItemKind::STRUCT),
1771                    label: "Particles".to_string(),
1772                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1773                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1774                        range: lsp::Range::default(),
1775                        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(),
1776                    })),
1777                    ..Default::default()
1778                },
1779                &language,
1780            )
1781            .await
1782            .unwrap();
1783
1784        assert_eq!(
1785            res,
1786            CodeLabel::new(
1787                "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
1788                0..9,
1789                vec![
1790                    (19..22, HighlightId::TABSTOP_INSERT_ID),
1791                    (31..34, HighlightId::TABSTOP_INSERT_ID),
1792                    (43..46, HighlightId::TABSTOP_INSERT_ID),
1793                    (55..58, HighlightId::TABSTOP_INSERT_ID),
1794                    (67..69, HighlightId::TABSTOP_REPLACE_ID),
1795                    (78..80, HighlightId::TABSTOP_REPLACE_ID),
1796                    (88..91, HighlightId::TABSTOP_INSERT_ID),
1797                    (0..9, highlight_type),
1798                    (60..65, highlight_field),
1799                    (71..76, highlight_field),
1800                ],
1801            )
1802        );
1803    }
1804
1805    #[gpui::test]
1806    async fn test_rust_label_for_symbol() {
1807        let adapter = Arc::new(RustLspAdapter);
1808        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1809        let grammar = language.grammar().unwrap();
1810        let theme = SyntaxTheme::new_test([
1811            ("type", Hsla::default()),
1812            ("keyword", Hsla::default()),
1813            ("function", Hsla::default()),
1814            ("property", Hsla::default()),
1815        ]);
1816
1817        language.set_theme(&theme);
1818
1819        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1820        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1821        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1822
1823        assert_eq!(
1824            adapter
1825                .label_for_symbol(
1826                    &language::Symbol {
1827                        name: "hello".to_string(),
1828                        kind: lsp::SymbolKind::FUNCTION,
1829                        container_name: None,
1830                    },
1831                    &language
1832                )
1833                .await,
1834            Some(CodeLabel::new(
1835                "fn hello".to_string(),
1836                3..8,
1837                vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1838            ))
1839        );
1840
1841        assert_eq!(
1842            adapter
1843                .label_for_symbol(
1844                    &language::Symbol {
1845                        name: "World".to_string(),
1846                        kind: lsp::SymbolKind::TYPE_PARAMETER,
1847                        container_name: None,
1848                    },
1849                    &language
1850                )
1851                .await,
1852            Some(CodeLabel::new(
1853                "type World".to_string(),
1854                5..10,
1855                vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1856            ))
1857        );
1858
1859        assert_eq!(
1860            adapter
1861                .label_for_symbol(
1862                    &language::Symbol {
1863                        name: "zed".to_string(),
1864                        kind: lsp::SymbolKind::PACKAGE,
1865                        container_name: None,
1866                    },
1867                    &language
1868                )
1869                .await,
1870            Some(CodeLabel::new(
1871                "extern crate zed".to_string(),
1872                13..16,
1873                vec![(0..6, highlight_keyword), (7..12, highlight_keyword),],
1874            ))
1875        );
1876
1877        assert_eq!(
1878            adapter
1879                .label_for_symbol(
1880                    &language::Symbol {
1881                        name: "Variant".to_string(),
1882                        kind: lsp::SymbolKind::ENUM_MEMBER,
1883                        container_name: None,
1884                    },
1885                    &language
1886                )
1887                .await,
1888            Some(CodeLabel::new(
1889                "Variant".to_string(),
1890                0..7,
1891                vec![(0..7, highlight_type)],
1892            ))
1893        );
1894    }
1895
1896    #[gpui::test]
1897    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1898        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1899        cx.update(|cx| {
1900            let test_settings = SettingsStore::test(cx);
1901            cx.set_global(test_settings);
1902            cx.update_global::<SettingsStore, _>(|store, cx| {
1903                store.update_user_settings(cx, |s| {
1904                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
1905                });
1906            });
1907        });
1908
1909        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1910
1911        cx.new(|cx| {
1912            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1913
1914            // indent between braces
1915            buffer.set_text("fn a() {}", cx);
1916            let ix = buffer.len() - 1;
1917            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1918            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1919
1920            // indent between braces, even after empty lines
1921            buffer.set_text("fn a() {\n\n\n}", cx);
1922            let ix = buffer.len() - 2;
1923            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1924            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1925
1926            // indent a line that continues a field expression
1927            buffer.set_text("fn a() {\n  \n}", cx);
1928            let ix = buffer.len() - 2;
1929            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1930            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1931
1932            // indent further lines that continue the field expression, even after empty lines
1933            let ix = buffer.len() - 2;
1934            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1935            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1936
1937            // dedent the line after the field expression
1938            let ix = buffer.len() - 2;
1939            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1940            assert_eq!(
1941                buffer.text(),
1942                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1943            );
1944
1945            // indent inside a struct within a call
1946            buffer.set_text("const a: B = c(D {});", cx);
1947            let ix = buffer.len() - 3;
1948            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1949            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1950
1951            // indent further inside a nested call
1952            let ix = buffer.len() - 4;
1953            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1954            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1955
1956            // keep that indent after an empty line
1957            let ix = buffer.len() - 8;
1958            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1959            assert_eq!(
1960                buffer.text(),
1961                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1962            );
1963
1964            buffer
1965        });
1966    }
1967
1968    #[test]
1969    fn test_package_name_from_pkgid() {
1970        for (input, expected) in [
1971            (
1972                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1973                "zed",
1974            ),
1975            (
1976                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1977                "my-custom-package",
1978            ),
1979        ] {
1980            assert_eq!(package_name_from_pkgid(input), Some(expected));
1981        }
1982    }
1983
1984    #[test]
1985    fn test_target_info_from_metadata() {
1986        for (input, absolute_path, expected) in [
1987            (
1988                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"}]}]}"#,
1989                "/path/to/zed/src/main.rs",
1990                Some((
1991                    Some(TargetInfo {
1992                        package_name: "zed".into(),
1993                        target_name: "zed".into(),
1994                        required_features: Vec::new(),
1995                        target_kind: TargetKind::Bin,
1996                    }),
1997                    Arc::from("/path/to/zed".as_ref()),
1998                )),
1999            ),
2000            (
2001                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"}]}]}"#,
2002                "/path/to/custom-package/src/main.rs",
2003                Some((
2004                    Some(TargetInfo {
2005                        package_name: "my-custom-package".into(),
2006                        target_name: "my-custom-bin".into(),
2007                        required_features: Vec::new(),
2008                        target_kind: TargetKind::Bin,
2009                    }),
2010                    Arc::from("/path/to/custom-package".as_ref()),
2011                )),
2012            ),
2013            (
2014                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"}]}"#,
2015                "/path/to/custom-package/src/main.rs",
2016                Some((
2017                    Some(TargetInfo {
2018                        package_name: "my-custom-package".into(),
2019                        target_name: "my-custom-bin".into(),
2020                        required_features: Vec::new(),
2021                        target_kind: TargetKind::Example,
2022                    }),
2023                    Arc::from("/path/to/custom-package".as_ref()),
2024                )),
2025            ),
2026            (
2027                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"]}]}]}"#,
2028                "/path/to/custom-package/src/main.rs",
2029                Some((
2030                    Some(TargetInfo {
2031                        package_name: "my-custom-package".into(),
2032                        target_name: "my-custom-bin".into(),
2033                        required_features: vec!["foo".to_owned(), "bar".to_owned()],
2034                        target_kind: TargetKind::Example,
2035                    }),
2036                    Arc::from("/path/to/custom-package".as_ref()),
2037                )),
2038            ),
2039            (
2040                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"}]}"#,
2041                "/path/to/custom-package/src/main.rs",
2042                Some((
2043                    Some(TargetInfo {
2044                        package_name: "my-custom-package".into(),
2045                        target_name: "my-custom-bin".into(),
2046                        required_features: vec![],
2047                        target_kind: TargetKind::Example,
2048                    }),
2049                    Arc::from("/path/to/custom-package".as_ref()),
2050                )),
2051            ),
2052            (
2053                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"}]}"#,
2054                "/path/to/custom-package/src/main.rs",
2055                Some((None, Arc::from("/path/to/custom-package".as_ref()))),
2056            ),
2057        ] {
2058            let metadata: CargoMetadata = serde_json::from_str(input).context(input).unwrap();
2059
2060            let absolute_path = Path::new(absolute_path);
2061
2062            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
2063        }
2064    }
2065
2066    #[test]
2067    fn test_rust_test_fragment() {
2068        #[track_caller]
2069        fn check(
2070            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
2071            path: &str,
2072            expected: &str,
2073        ) {
2074            let path = Path::new(path);
2075            let found = test_fragment(
2076                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
2077                path,
2078                path.file_stem().unwrap().to_str().unwrap(),
2079            );
2080            assert_eq!(expected, found);
2081        }
2082
2083        check([], "/project/src/lib.rs", "--lib");
2084        check([], "/project/src/foo/mod.rs", "foo");
2085        check(
2086            [
2087                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
2088                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
2089            ],
2090            "/project/src/main.rs",
2091            "--bin=x",
2092        );
2093        check([], "/project/src/main.rs", "--");
2094    }
2095
2096    #[test]
2097    fn test_convert_rust_analyzer_schema() {
2098        let raw_schema = serde_json::json!([
2099            {
2100                "title": "Assist",
2101                "properties": {
2102                    "rust-analyzer.assist.emitMustUse": {
2103                        "markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.",
2104                        "default": false,
2105                        "type": "boolean"
2106                    }
2107                }
2108            },
2109            {
2110                "title": "Assist",
2111                "properties": {
2112                    "rust-analyzer.assist.expressionFillDefault": {
2113                        "markdownDescription": "Placeholder expression to use for missing expressions in assists.",
2114                        "default": "todo",
2115                        "type": "string"
2116                    }
2117                }
2118            },
2119            {
2120                "title": "Cache Priming",
2121                "properties": {
2122                    "rust-analyzer.cachePriming.enable": {
2123                        "markdownDescription": "Warm up caches on project load.",
2124                        "default": true,
2125                        "type": "boolean"
2126                    }
2127                }
2128            }
2129        ]);
2130
2131        let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema);
2132
2133        assert_eq!(
2134            converted.get("type").and_then(|v| v.as_str()),
2135            Some("object")
2136        );
2137
2138        let properties = converted
2139            .pointer("/properties")
2140            .expect("should have properties")
2141            .as_object()
2142            .expect("properties should be object");
2143
2144        assert!(properties.contains_key("assist"));
2145        assert!(properties.contains_key("cachePriming"));
2146        assert!(!properties.contains_key("rust-analyzer"));
2147
2148        let assist_props = properties
2149            .get("assist")
2150            .expect("should have assist")
2151            .pointer("/properties")
2152            .expect("assist should have properties")
2153            .as_object()
2154            .expect("assist properties should be object");
2155
2156        assert!(assist_props.contains_key("emitMustUse"));
2157        assert!(assist_props.contains_key("expressionFillDefault"));
2158
2159        let emit_must_use = assist_props
2160            .get("emitMustUse")
2161            .expect("should have emitMustUse");
2162        assert_eq!(
2163            emit_must_use.get("type").and_then(|v| v.as_str()),
2164            Some("boolean")
2165        );
2166        assert_eq!(
2167            emit_must_use.get("default").and_then(|v| v.as_bool()),
2168            Some(false)
2169        );
2170
2171        let cache_priming_props = properties
2172            .get("cachePriming")
2173            .expect("should have cachePriming")
2174            .pointer("/properties")
2175            .expect("cachePriming should have properties")
2176            .as_object()
2177            .expect("cachePriming properties should be object");
2178
2179        assert!(cache_priming_props.contains_key("enable"));
2180    }
2181}