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