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