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