rust.rs

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