rust.rs

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